【iOS】类与对象底层探索

文章目录

  • 前言
  • 一、编译源码
  • 二、探索对象本质
  • 三、objc_setProperty 源码探索
  • 四、类 & 类结构分析
    • isa指针是什么
    • 类的分析
    • 元类
    • 元类的说明
  • 五、著名的isa走位 & 继承关系图
  • 六、objc_class & objc_object
    • objc_class结构
    • superClass
    • bits
    • class_rw_t
    • class_ro_t
    • ro与rw的区别
    • class_rw_ext_t
    • cache_t结构
  • 总结


前言

这篇文章主要探索OC对象的本质

首先我们需要明白我们平时编写的OC代码,底层实现都是C\C++代码

一、编译源码

首先通过终端利用clangmain.m编译为main.cpp

//1、将 main.m 编译成 main.cpp
clang -rewrite-objc main.m -o main.cpp//2、将 ViewController.m 编译成  ViewController.cpp
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot / /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.7.sdk ViewController.m//以下两种方式是通过指定架构模式的命令行,使用xcode工具 xcrun
//3、模拟器文件编译
- xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp //4、真机文件编译
- xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp 

二、探索对象本质

我们打开编译好的源文件后找到LGPerson,发现其在底层被编译为struct结构体

//NSObject的定义
@interface NSObject <NSObject> {Class isa  OBJC_ISA_AVAILABILITY;
}//NSObject 的底层编译
struct NSObject_IMPL {Class isa;
};//LGPerson的底层编译
struct LGPerson_IMPL {struct NSObject_IMPL NSObject_IVARS; // 等效于 Class isa;NSString *_name;
};

LGPerson_IMPL实现结构体中的第一个属性是isa,是继承自NSObject,是伪继承。意味着LGPerson拥有者NSObject中所有成员变量

LGPerson中的第一个属性 NSObject_IVARS 等效于 NSObject中的 isa

这里也许我们会产生一个疑问就是为什么isa的类型是class
根本原因是由于isa 对外反馈的是类信息

总结
因此我们可以得出:

OC对象本质就是结构体
LGPerson中的isa就是继承自NSObject中的isa

三、objc_setProperty 源码探索

除了LGPerson的底层定义,我们发现了属性name还有set与get方法,其中set方法依赖于runtime中的objc_setProperty

我们通过源码来查看一下objc_setProperty的底层实现

在这里插入图片描述

// 定义静态内联函数,用于设置对象的属性值。
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{// 如果偏移量为0,直接将newValue设置为对象的类(可能用于特殊的目的,如改变对象的动态类型)if (offset == 0) {object_setClass(self, newValue);return;}// 定义用来保存旧值的变量id oldValue;// 计算属性值在内存中的实际位置id *slot = (id*) ((char*)self + offset);// 如果指定了copy标志,则对newValue执行不可变拷贝if (copy) {newValue = [newValue copyWithZone:nil];} // 如果指定了mutableCopy标志,则对newValue执行可变拷贝else if (mutableCopy) {newValue = [newValue mutableCopyWithZone:nil];} // 如果没有指定拷贝,检查newValue是否已经是当前值,如果是,则无需操作;否则,增加newValue的引用计数else {if (*slot == newValue) return;newValue = objc_retain(newValue);}// 如果不是原子操作,直接更新内存位置的值if (!atomic) {oldValue = *slot;*slot = newValue;} // 如果是原子操作,使用锁来保证线程安全else {spinlock_t& slotlock = PropertyLocks[slot]; // 获取与属性位置相关联的锁slotlock.lock();                            // 锁定oldValue = *slot;                          // 取出旧值*slot = newValue;                          // 设置新值slotlock.unlock();                         // 解锁}// 释放旧值的引用,以防内存泄漏objc_release(oldValue);
}

其方法原理就是retain新值->设置新值->释放旧值

四、类 & 类结构分析

本篇章的主要目的是分析 类 & 类的结构,整篇都是围绕一个类展开的一些探索

isa指针是什么

OC是一门面向对象编程的语言,每个对象都是类的实例,同时也被称为实例对象,同时每个对象都有一个isa指针,指向对象所属的类

另外我们打开NSObject源码

@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}

由此可知类也是一个对象,简称为类对象

类的分析

首先我们定义两个类

继承自NSObject的类CJLPerson

@interface CJLPerson : NSObject
{NSString *hobby;
}
@property (nonatomic, copy) NSString *cjl_name;
- (void)sayHello;
+ (void)sayBye;
@end@implementation CJLPerson
- (void)sayHello
{}
+ (void)sayBye
{}
@end

继承自CJLPerson的类CJLTeacher

@interface CJLTeacher : CJLPerson
@end@implementation CJLTeacher
@end

在main中分别用两个定义两个对象:person & teacher

int main(int argc, const char * argv[]) {@autoreleasepool {//ISA_MASK  0x00007ffffffffff8ULLCJLPerson *person = [CJLPerson alloc];CJLTeacher *teacher = [CJLTeacher alloc];NSLog(@"Hello, World! %@ - %@",person,teacher);  }return 0;
}

元类

首先我们通过一张图片引入元类
在这里插入图片描述

根据调试过程,我们产生了一个疑问:为什么图中的p/x 0x001d8001000022dd & 0x00007ffffffffff8ULLp/x 0x00000001000022b0 & 0x00007ffffffffff8ULL 中的类信息打印出来都是CJLPerson

  • 0x001d8001000022ddperson对象的isa指针地址,其&后得到的结果是 创建person的类CJLPerson
  • 0x00000001000022b0isa中获取的类信息所指的类的isa的指针地址,即 CJLPerson类的类 的isa指针地址,在Apple中,我们简称CJLPerson类的类为 元类
    所以,两个打印都是CJLPerson的根本原因就是因为元类导致的

元类的说明

下面我们来解释一下什么是元类

首先我们知道对象的isa指向类,同时我们前文也说了类也是一个对象,那么类也有isa指针,类的isa指针指向的就是元类

元类是系统给的,当我们创建类时会自动创建元类,类的归属来自于元类

首先我们之前的博客有分析过元类【iOS】isKindOfClass & isMemberOfClass比较
我们这里引出一个问题,NSObject到底有几个?

在这里插入图片描述
从这张图中我们可以看出根元类NSObject只有一个,这个与我们日常开发的NSObject是同一个吗

我们通过代码来验证一下
在这里插入图片描述

可以看出打印出的地址为同一个,所以NSObject只有一个,即类对象与元类都只有一个,而实例对象可以有很多个

 NSObject *object1 = [[NSObject alloc] init];NSObject *object2 = [[NSObject alloc] init];// 打印两个实例的内存地址NSLog(@"object1: %p", object1);NSLog(@"object2: %p", object2);// 获取并打印NSObject的类对象Class objectClass1 = [object1 class];Class objectClass2 = [object2 class];NSLog(@"NSObject class: %p. %p", objectClass1, objectClass2);// 获取并打印NSObject的元类对象Class metaClass1 = object_getClass(objectClass1);Class metaClass2 = object_getClass(objectClass2);NSLog(@"NSObject meta-class: %p, %p", metaClass1, metaClass2);

[面试题]:类存在几份?

由于类的信息在内存中永远只存在一份,所以 类对象只有一份

五、著名的isa走位 & 继承关系图

在这里插入图片描述
这里的知识之前已经说过了,这里不再赘述

六、objc_class & objc_object

isa的走位我们清楚了,现在我们来讨论一个新问题,为什么对象和类会有isa属性,这里就引出了objc_class & objc_object

首先我们通过之前的源码知道:
NSObject的底层编译是NSObject_IMPL结构体

首先我们知道Classisa指针的类型,是由objc_class定义的类型
objc_class实际上就是Class
在iOS中所有的Class都是以objc_class为模版创建的

我们查看一下objc_class的源码
在这里插入图片描述
我们可以看到objc_class结构体是继承自objc_object

再搜寻objc_object的定义
在这里插入图片描述

【问题】objc_class 与 objc_object 有什么关系?
通过查看源码我们可以得出几点说明:

  • objc_class继承自objc_object类型,其中objc_object也是一个结构体,且有一个isa类型
  • 在cpp底层编译中,isa的类型是Class,class的底层编码来自objc_class,因此NSObject也有了isa属性
  • objc_object(结构体)是当前的根对象,所有的对象都有一个特性objc_object,即拥有isa属性

objc_object 与 对象的关系

  • 是一个继承关系,所有对象都是由objc_object继承过来的

  • 所有的对象都是来自NSObject,但最后到底层都是一个objc_object(C/C++)结构体类型

总结:
所有对象+类+元类都有isa属性
所有对象都是由objc_object继承来的

objc_class结构

首先我们在源码中找到其结构

struct objc_class : objc_object {// Class ISA;Class superclass;cache_t cache;             // formerly cache pointer and vtableclass_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
}

isa:主要指向类对象或是元类对象
superclass:指向当前类的父类
cache:方法缓存,提高调用方法的性能
bits:封装了类的其他信息,例如成员变量,方法列表,协议,属性

元类对象结构也是这样,只不过元类对象里面存放的是类方法

superClass

这里需要注意superclass是objc_class特有的,实例对象是没有的

bits

类结构中还有一个bits
我们通过源码查看一下其数据结构class_data_bits_t

struct class_data_bits_t {friend objc_class;// Values are the FAST_ flags above.uintptr_t bits;public:class_rw_t* data() const {return (class_rw_t *)(bits & FAST_DATA_MASK);}// Get the class's ro data, even in the presence of concurrent realization.// fixme this isn't really safe without a compiler barrier at least// and probably a memory barrier when realizeClass changes the data fieldconst class_ro_t *safe_ro() const {class_rw_t *maybe_rw = data();if (maybe_rw->flags & RW_REALIZED) {// maybe_rw is rwreturn maybe_rw->ro();} else {// maybe_rw is actually roreturn (class_ro_t *)maybe_rw;}}
}

其中最重要的两个方法就是datasafe_ro,两个方法分别返回class_rw_tclass_ro_t

class_rw_t

在这里插入图片描述
首先可以看到三个方法,可以分别获取到类的方法,属性,协议

class_ro_t

在这里插入图片描述
可以看到有方法、属性、协议和成员变量。但方法、属性、协议的命名都是base开通的

ro与rw的区别

ro是编译阶段生成,rw是运行阶段生成,从存储的角度来说,ro中有方法,属性,协议与成员变量,而rw中没有成员变量,rw中的取值方法也是通过取ro或是rwe的值来获得的

class_rw_ext_t

在2020WWDC中有个视频对ro与rw进行了解释,由于rw是一块脏内存,但是rw总有许多用不到的数据,我们将rw中那些一般用不到的数据分离出来变为干净的内存,也就是rw_ext_t,这样就减轻了rw对内存的占用

rw_ext_t生成条件:

  • 使用分类的类

  • 使用Runtime API动态修改类的结构的时候

这两种情况时,由于类的结构发生改变,但是ro是只读的,因此需要重新生成可读可写的内存结构rw_ext(Dirty Memory, 比较贵),来存放新的类结构。

由此再次读取方法,属性,列表时如果有rw_ext,就会先从rw_ext中读取,如果没有再去读取ro

参考博客:
iOS八股文(四)类对象的结构(下)

思考:为什么类方法存储在元类中,而不把类方法存储在类对象中?或者说设置元类的目的是什么?

  1. 单一职责设计原理:一个对象或是一个类只应该有一个职责,类对象负责实例对象的行为,例如实例方法,协议,成员变量,属性等,元类对象负责类对象的行为,负责存放类方法,各司其职互不影响
  2. 符合消息转发机制
  3. 继承模型的一致性:在Objective-C中,类方法的继承与实例方法的继承遵循相同的模式。如果没有元类,类方法的继承将需要另外一套机制来处理。

cache_t结构

cache的作用是在objc_msgSend过程中会先在cache中根据方法名来hash查找方法实现,如果能查找到就直接掉用。如果查找不到然后再去rw_t中查找。然后再在cache中缓存。

总结

通过这篇文章我们得知

  1. 所有对象都有isa属性
  2. 所有对象都是由objc_object继承而来的
  3. objc_class中存放着对象的各种信息,实例对象则存放成员变量,类对象则存放实例方法与属性等,元类对象则存放类方法,符合单一职责原则
  4. isa 指针指向对象所属的类
  5. 可以从bits中的rw中查找方法属性,但不能查找到成员变量,成员变量存储在ro中,这也是为什么分类不能添加成员变量的原因

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/4021.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

在Elasticsearch 7.9.2中安装IK分词器并进行自定义词典配置

Elasticsearch是一个强大的开源搜索引擎&#xff0c;而IK分词器是针对中文文本分析的重要插件。本文将引导您完成在Elasticsearch 7.9.2版本中安装IK分词器、配置自定义词典以及验证分词效果的全过程。 步骤一&#xff1a;下载IK分词器 访问IK分词器的GitHub发布页面&#xf…

游戏发行困境及OgGame云游戏解决方案简述

随着全球化浪潮的持续推进&#xff0c;中国游戏开发者们不再满足于国内市场的发展&#xff0c;而是开始将目光投向更为广阔的海外市场。这一趋势的崛起背后&#xff0c;是中国企业意识到国际化是其发展的必由之路&#xff0c;也是游戏行业突破国内困境的体现。本文将简要阐述游…

发那科FANUC机器人R-2000iB平衡缸维修攻略

在发那科机器人中&#xff0c;平衡缸扮演着稳定机械臂运动的关键角色。它通过内部的压力调节来平衡负载&#xff0c;保证机器人的精准定位和平稳操作。一旦出现法兰克机械手平衡缸故障或损坏&#xff0c;机器人的性能可能会大打折扣&#xff0c;因此及时且正确的FANUC机械手平衡…

并并并并·病查坤

P1、什么是并查集 引用自百度百科&#xff1a; 并查集&#xff0c;在一些有N个元素的集合应用问题中&#xff0c;我们通常是在开始时让每个元素构成一个单元素的集合&#xff0c;然后按一定顺序将属于同一组的元素所在的集合合并&#xff0c;其间要反复查找一个元素在哪个集合…

银行押款车远程监控系统的实际需求与特点

随着金融行业的快速发展&#xff0c;银行押款车的安全性问题日益受到重视。传统的押款车监控方式已经无法满足现代安全管理的需求&#xff0c;因此&#xff0c;一种结合先进技术的远程监控系统应运而生。本文旨在探讨在运钞车上安装车载摄像机和集成有GPS、无线4G网络传输模块的…

C语言 switch语句

之前 我们讲了 if 和 嵌套的if分支语句 但其实 多分支语句 我们还可以用 switch 有时 switch 语句可以简化逻辑代码 switch语句也称之为开关语句&#xff0c;其像多路开关一样&#xff0c;使程序控制流程形成多个分支&#xff0c;根据一个表达式的不同取值&#xff0c;选择其…

社交巨头与去中心化:解析Facebook在区块链的角色

在数字化时代&#xff0c;社交媒体已经成为人们日常生活中不可或缺的一部分。作为全球最大的社交媒体平台&#xff0c;Facebook 在社交领域的影响力无可置疑。然而&#xff0c;随着区块链技术的崛起&#xff0c;Facebook 也开始探索如何将这一技术应用于其平台&#xff0c;以适…

UE4_动画基础_FootIK

角色由于胶囊体的阻挡&#xff0c;双脚与地面平行&#xff0c;不会与斜坡、台阶等贴合&#xff0c;有一条腿会处于悬空状态&#xff0c;通过双骨骼IK节点使一只脚太高&#xff0c;让后胶囊体下降&#xff0c;修正双脚的角度。这就是逆向运动IK的方法。 一、新建第三人称模板游戏…

微信小程序开发:2.小程序组件

常用的视图容器类组件 View 普通的视图区域类似于div常用来进行布局效果 scroll-view 可以滚动的视图&#xff0c;常用来进行滚动列表区域 swiper and swiper-item 轮播图的容器组件和轮播图的item项目组件 View组件的基本使用 案例1 <view class"container"&…

【ARM 裸机】NXP 官方 SDK 使用

在前几节中&#xff0c;学习了如何编写汇编的 led 驱动、C 语言的 led 驱动、模仿 STM32 进行开发&#xff0c;我们都是自己写外设寄存器的结构体&#xff0c;外设非常的多&#xff0c;写起来费时费力&#xff0c;NXP 针对 I.MX6ULL 编写了一个 SDK 包&#xff0c;这个 SDK 包就…

解析Python中获取当前线程名字的方法及多线程编程实践

&#x1f47d;发现宝藏 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。【点击进入巨牛的人工智能学习网站】。 Python 获取当前线程的名字 在多线程编程中&#xff0c;了解当前线程的名字是一项重要的任…

SQL异常

异常 EXCEPTION 预定义异常 系统已经设置好的异常&#xff0c;包含了异常名&#xff0c;异常代码&#xff0c;异常信息组成 CASE NOT FOUND 未知异常&#xff1a;OTHERS 异常信息&#xff1a;SQLERRM 错误代码&#xff1a;SQLCODE 有各种各样的很多异常 捕获异常的语法 DE…

Redis高级篇详细讲解

0.今日菜单 Redis持久化【理解】 Redis主从 Redis哨兵 Redis分片集群【运维】 单点Redis的问题 数据丢失问题&#xff1a;Redis是内存存储&#xff0c;服务重启可能会丢失数据 并发能力问题&#xff1a;单节点Redis并发能力虽然不错&#xff0c;但也无法满足如618这样的高…

C++链表操作入门

数据结构基础&#xff1a;链表操作入门 数据结构基础&#xff1a;链表操作入门链表的基本概念链表的基本操作输出链表插入节点删除节点查找值 完整的链表操作示例结语 数据结构基础&#xff1a;链表操作入门 在计算机科学中&#xff0c;数据结构是组织和存储数据的方式&#x…

【Linux学习】​​学习Linux的准备工作和Linux的基本指令

˃͈꒵˂͈꒱ write in front ꒰˃͈꒵˂͈꒱ ʕ̯•͡˔•̯᷅ʔ大家好&#xff0c;我是xiaoxie.希望你看完之后,有不足之处请多多谅解&#xff0c;让我们一起共同进步૮₍❀ᴗ͈ . ᴗ͈ აxiaoxieʕ̯•͡˔•̯᷅ʔ—CSDN博客 本文由xiaoxieʕ̯•͡˔•̯᷅ʔ 原创 CSDN 如…

C语言:插入排序

插入排序 1.解释2.步骤3.举例分析示例结果分析 1.解释 插入排序是一种简单直观的排序算法&#xff0c;它的工作原理是通过构建有序序列&#xff0c;对于未排序数据&#xff0c;在已排序序列中从后向前扫描&#xff0c;找到相应位置并插入。插入排序在实现上&#xff0c;通常采…

直播带货秘籍:人气飙升的成交话术大揭秘

在营销的广阔天地中&#xff0c;种草话术如同一把锐利的剑&#xff0c;能精准切入消费者的心智。选择恰当的切入点是关键&#xff0c;它可能是一个普遍的生活场景&#xff0c;一个共同的消费痛点&#xff0c;或是一处人们向往的心理寄托。通过细致的观察和分析&#xff0c;我们…

Markdown 列表语法

有序列表 要创建有序列表&#xff0c;请在每个列表项前添加数字并紧跟一个英文句点。数字不必按数学顺序排列&#xff0c;但是列表应当以数字 1 起始。 Markdown语法HTML预览效果1. First item 2. Second item 3. Third item 4. Fourth item<ol> <li>First item&…

Apollo 7周年大会:百度智能驾驶的展望与未来

本心、输入输出、结果 文章目录 Apollo 7周年大会&#xff1a;百度智能驾驶的展望与未来前言百度集团副总裁、智能驾驶事业群组总裁王云鹏发言 直播回放大会相关内容先了解 Apollo&#xfeff;开放平台 9.0架构图 发布产品Apollo 定义自己对于智能化的认知百度集团副总裁 王云鹏…

沉浸式推理乐趣:体验线上剧本杀小程序的魅力

在这个信息爆炸的时代&#xff0c;人们的娱乐方式也在不断地推陈出新。其中&#xff0c;线上剧本杀小程序以其独特的沉浸式推理乐趣&#xff0c;成为了许多人的新宠。它不仅让我们在闲暇之余享受到了推理的快乐&#xff0c;更让我们在虚拟的世界里感受到了人性的复杂与多彩。 线…