前言
通常在创建对象的时候,都会继承 NSObject去新建一个类,那么NSObject 继承谁?或者说类的底层原理是什么?下面来具体探究一下。
本文探索过程会涉及到 对象的本质
准备工作
- 新建一个
Project
- 在
main.m中添加一个类ZLObject,打上断点并执行。
案例分析
1. 探索对象的底层
$ p obj:查看obj对象的地址。
$ x/4gx obj地址:查看obj对象的isa及内存占用。
$ p/x isa地址 & 掩码地址:与掩码做与运算
$ po 与运算地址:查看关联类
流程如下:
其中掩码为
__86_64__的掩码地址0x00007ffffffffff8ULL,最终得到ZLObject的地址:0x0000000100008260
2. 继续探索
以上面
ZLObject的0x0000000100008260地址,再次进行isa和ISA_MASK与运算,最终得到0x0000000100008238的地址,还是ZLObject。
这就比较奇怪了,为什么都是 ZLObject,内存地址却不一样?
3. 再次探索
再次以新的
ZLObject的0x0000000100008238地址,再次进行isa和ISA_MASK与运算,最终得到0x00007fff92c9d0f0的地址,是NSObject。
对此,有两个疑问:
两次的
ZLObject是否存在的一定的联系?
NSObject的isa指向了什么?
4. 方法印证
添加下图方法,并打印其内存情况。
打印结果如下:
通过上面的案例,得到的结论是:对象的
isa是指向类,也就是ZLObject的内存地址0x0000000100008260,那么类的isa指向的是什么?为什么这块内存地址也是ZLObject。
5. MachO文件分析
有关MachO文件探索,请移步 MachO文件分析
通过
MachOView的分析,直接定义到Symbol Table下查看所有的symbols数据,搜索class,得出:
- 找到了
0x0000000100008260的内存地址,其符号下标就是_OBJC_CLASS_$_ZLObject,也就是ZLObject。- 找到了
0x0000000100008238的内存地址,其符号下标就是_OBJC_METACLASS_$_ZLObject,称为ZLObject的元类。
结论一
对象的
isa指向类类的
isa指向元类
元类是系统编译器生成的。
MetaClass的本质
上面的分析中,提出了两个疑问,其中第一个已经证实,两次的 ZLObject,一个是 类,一个是 元类。那么,NSObject 的 isa 指向了什么?接下来我们继续探索。
通过查看 NSObject.class 的内存地址 0x00007fff92c9d118,发现和之前的 NSObject 地址 0x00007fff92c9d0f0 不一样,因此 0x00007fff92c9d0f0 为 NSObject 的 元类。
那么 NSObject元类 的 isa 又指向了什么?
通过运算分析,NSObject元类 的 isa 还是指向 NSObject元类。
结论二
元类的isa指向根元类
根元类的isa还是指向根元类对于
NSObject,它也是根类,根类的isa也是指向根元类
SuperClass的本质
上面分析了 ZLObject 类和 NSObject类的 isa 指向情况,那么父类 SuperClass 的 isa 指向情况和继承关系如何呢?
1. NSObject SuperClass
创建如图类,并打印其内存情况:
打印结果如下:
说明:
NSObject的父类是nil
NSObject的元类的父类还是NSObject
2. ZLObject SuperClass
首先看一下类的父类是 NSObject 的情况,打印其元类:
打印结果如下:
说明:
ZLObject的元类的父类是NSObject的元类
3. ZLSubObject SuperClass
创建继承于 ZLObject 的子类 ZLSubObject,并打印如图内存:
打印情况如下:
说明:
ZLSubObject的元类的父类是ZLObject的元类。
结论三
NSObject的父类是nil,其元类的父类还是NSObject父类的元类也有继承关系。
最终得到两个关系图,一个是类的 isa 指向图,一个是类的继承链图。
内存偏移
1. 普通指针
打印结果:
说明:常量10处于
常量区,可以被不同的指针引用,其引用原理为值拷贝。
2. 数组指针
打印结果:
说明:
使用数组
下标取地址,和利用指针偏移量取值效果一样。不如上图中是&c[0]和b + 1数组的
首地址也就是数组第一个元素的地址。指针
偏移量大小和数组元素所占用字节大小有关,比如上图中是int,所以打印结果地址相差4,也称步长。
类的内存结构
分析源码可知,objc_class 方法实现如下:
其内存结构图如下:
因此如果想要得到 bits,就必须知道 superclass 和 cache 的内存字节数,再利用内存偏移得到 bits。
由于 isa 是 8 字节,此处不再赘述。
1. superclass
superclass 和 isa 一样,也是 8 字节,因为都是 Class 结构体类型
2. cache
cache_t 的有效代码如下:
cache_t中的方法和static声明的字段都不是在该结构体内,所以只需要分析上面的有效代码,获取cache_t所占用的内存大小,即可得到bits的内存偏移量。
1. 联合体之外的 explicit_atomic<uintptr_t>的大小:
explicit_atomic 为泛型指针,所以其内存大小由 <uintptr_t> 决定的,也就是 uintptr_t 的大小,为 8 字节。也可以使用 sizeof(uintptr_t) 查看其字节大小。
2. 联合体的大小:
说明:
mask_t为uint32_t类型,所以为4,uint16_t类型为2。
<preopt_cache_t *>为结构体指针,所以为8。联合体内部
内存共用,且互斥的特性,所以总大小为8。
结论
cache所占字节为16
bits的内存偏移量为isa+superclass+cache,总大小为32
类的底层数据获取
1. 获取 bits 数据:
上图可得类的首地址为:0x1000081e8,那么以类的首地址偏移 32 字节,就是 bits 。
此时 bits 的数据存储在 $4 中。
在上面分析 objc_class 的结构时,可以对 data() 进行存取数据。
因此,可以通过 $4->data() 方法获取 class_rw_t 内存地址:
2. 获取 class_rw_t 数据:
这样 class_rw_t 的数据就可以拿到了,但是并不知道所需的属性和方法具体存在哪里。
3. 分析 class_rw_t 结构体:
通过 properties() 和 methods() 获取类的属性和方法。
4. 获取 property_array_t 的 list :
5. 获取 list 的 ptr :
6. 获取 property_list_t 的数据 :
7. 使用 C++数组 get() 方法,获取类的属性 :
8. 同理获取类的 methods() :
最后一步的 get(0) 没有拿到数据,因此获取方法和属性不一样。