前言
通常在创建对象的时候,都会继承 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
的大小:
explicit_atomic
为泛型指针,所以其内存大小由
决定的,也就是 uintptr_t
的大小,为 8
字节。也可以使用 sizeof(uintptr_t)
查看其字节大小。
2. 联合体的大小:
说明:
mask_t
为uint32_t
类型,所以为4
,uint16_t
类型为2
。
为结构体指针,所以为
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)
没有拿到数据,因此获取方法和属性不一样。