携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情
前言
上一篇我们探索了对象的底层原理,并知道对象的内存地址分布,
对象的内存地址由isa + 对象的各个属性组成,如下图所示:
那类会不会也有自己的内存结构?答案:是的。Class本质是一个结构体指针typedef struct objc_class *,接下来探索一下结构体objc_class的底层实现。
源码分析类结构
在objc_libdylib.A源码库objc-runtime-new类中,找到了objc_class的底层实现,其中发现有4个比较关键的对象,如下图所示:
Class ISA:是继承至objc_objcet的一个属性Class superclass: 指向当前类的父类cache_t cache: 对当前类的一些方法的缓存,后续再深入探索class_data_bits_t bits:存储类的内存数据,包括属性,成员变量,方法,协议等
该文主要对Class ISA走位分析,以及验证class_data_bits_t bits存储类的内存属性。
isa走位图分析
首先来看一张非常经典了isa走位图,是不是以前似懂非懂,今天就彻底搞明白他。其中虚线表示指向,实线表示继承,咋一看还是有点乱,接下来把下图拆成两部分分析,指向和继承。
- isa 指向分析
由上图中可以发现:
实例对象isa指向了类类isa指向了该类的元类该类的元类指向了根元类根元类指向了根元类
特殊的NSObject isa直接指向根元类
通过两种方式验证:
① lldb验证,因为我使用的是模拟器,所以使用到的掩码是0x00007ffffffffff8ULL,可以源码全局搜索isa_mask查看对应的掩码
分析:
// 获取对象t的isa 0x011d800100008c8d 便是对象的isa地址
(lldb) x/4gx t
0x100a27890: 0x011d800100008c8d 0x0000000000000000
0x100a278a0: 0x0000000000000000 0x0000000000000000
// 对象isa & 掩码 =》 类地址。这里验证了 `实例对象isa`指向了`类`
(lldb) p/x 0x011d800100008c8d & 0x00007ffffffffff8ULL
(unsigned long long) $2 = 0x0000000100008c88
// x/4gx 类地址,查看类的内存分部, 0x0000000100008c60 类的isa地址
(lldb) x/4gx 0x0000000100008c88
0x100008c88: 0x0000000100008c60 0x0000000100008d78
0x100008c98: 0x0000000100f04c10 0x0002804400000003
// 类地址 & 掩码 =》 元类,元类是系统自己生成的。 这里验证了 `类isa`指向了`该类的元类`
(lldb) p/x 0x0000000100008c60 & 0x00007ffffffffff8ULL
(unsigned long long) $3 = 0x0000000100008c60
(lldb) po 0x0000000100008c60
DXJTeacher
// x/4gx 元类地址,查看元类的内存分部, 0x00000001008010f0 元类的isa地址
(lldb) x/4gx 0x0000000100008c60
0x100008c60: 0x00000001008010f0 0x0000000100008d50
0x100008c70: 0x0000000100a27db0 0x0004e03500000007
// 元类isa & 掩码 =》 根元类,这里验证了 `元类isa`指向了`根元类`
(lldb) p/x 0x00000001008010f0 & 0x00007ffffffffff8ULL
(unsigned long long) $5 = 0x00000001008010f0
// 0x00000001008010f0 打印的是NSObject,但是这个地址是元类的地址,下方是验证
(lldb) po 0x00000001008010f0
NSObject
// p/x NSObject,可以看到0x0000000100801140 是NSObject的类地址
(lldb) p/x NSObject.class
(Class) $7 = 0x0000000100801140
// x/4gx NSObject类地址,这里可以看到NSObject的根元类地址就是0x00000001008010f0
(lldb) x/4gx 0x0000000100801140
0x100801140: 0x00000001008010f0 0x0000000000000000
0x100801150: 0x0000000100c42160 0x0002801000000003
// 根元类isa & 掩码 无论多少次最终都是指向了根元类,验证了`根元类`指向了`根元类`
(lldb) p/x 0x00000001008010f0 & 0x00007ffffffffff8ULL
(unsigned long long) $8 = 0x00000001008010f0
(lldb) p/x 0x00000001008010f0 & 0x00007ffffffffff8ULL
(unsigned long long) $9 = 0x00000001008010f0
(lldb)
② 代码验证 object_class() 函数
通过代码验证
- isa 继承链
由上图中可以发现:
元类isa继承至该类的父元类该类的父元类继承至根元类根元类继承至NSObjectNSObject继承至nil
通过两种方式验证:
① lldb验证
② 代码验证
class_getSuperclass()函数
// isa 继承链
/**
对象是没有继承关系的;
类的继承链 类->父类->父类的父类->...->NSObject->nil 不是这次所关心的,我们要探索的是isa的继承链
类的isa指向的是元类,所以直接从元类开始探索
*/
void getISAInherit(**void**) {
// 获取类地址
Class tClass = DXJTeacher.class;
// 获取tClass的isa指向的类 =》元类
Class tMateClass = object_getClass(tClass);
// 获取元类tMateClass继承的父类 =》 父元类
Class super_tMateClass = class_getSuperclass(tMateClass);
// 如果这里有多层继承关系,依旧会找相应的父元类的父元类
// 获取父元类super_tMateClass继承的父类 =》 根元类
Class root_tMateClass = class_getSuperclass(super_tMateClass);
// 获取根元类root_tMateClass继承的父类 =》 根类
Class root = class_getSuperclass(root_tMateClass);
// 获取根类的父类 =》 nil
Class obj = class_getSuperclass(root);
NSLog(@"\n==== DXJTeacher isa 继承链 ====\ntClass:%p %@\ntMateClass: %p %@\nsuper_tMateClass:%p %@\nroot_tMateClass:%p %@\nroot:%p %@\nobj:%p %@\n",tClass, tClass, tMateClass, tMateClass,super_tMateClass, super_tMateClass,root_tMateClass, root_tMateClass, root, root, obj, obj);
}
获取类的内存数据(平移)
我们知道类是加载到内存五大区中的栈中, 栈的特点就是先进后出,并且内存空间是连续的。文章开始我们已经知道了类有比较重要的四个对象Class isa, Class superClass, cache_t cache, class_data_bits_t bits,并且也知道了isa是类的首地址,那现在我们利用栈的内存空间连续的特性,探索一下class_data_bits_t bits。相信工作很多年,会经常听到类的属性放在rw中,实例方法也放在rw中,成员变量放在ro中,类方法放在元类中,之所以探索class_data_bits_t bits,是因为我们想知道的rw ro rwe都在bits中,那现在就来验证一下是否是这样的?
获取class_data_bits_t bits
// Class ISA; 8字节
Class superclass; // 8字节
cache_t cache; // 16字节
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
根据栈空间的连续性,利用内存偏移获取bits地址,先获取到isa地址,这个我们已经轻车熟路了,下面计算下到bits的偏移量:
Class ISA,Class是一个结构体指针占8个字节Class superclass;Class是一个结构体指针占8个字节cache_t cache;cache_t占用16字节
分析cache_t为什么是16字节?如下图,cache_t结构体内容很多,但是绝大多数是方法,是不占结构体空间的,所以只剩下篮筐和红框两部分,篮筐使用sizeof(uintptr_t)打印8字节,红框中是一个联合体(互斥),联合体内部的结构体是共同体占用8字节(或者使用sizeof(preopt_cache_t)打印也是8字节),cache_t是结构体是共存的,所以内存大小事篮框➕红框的的大小即8字节+8字节 = 16字节
最终到bits的偏移量为: ISA + superClass + cache_t = 8 + 8 + 16 = 32,也就是拿到isa地址再偏移32字节就是bits的内存地址了。用16进制表示即为0x20
属性 类的属性放在rw的属性列表中
- 获取类地址
2. 偏移
0x20
3. 将获取到的地址强转为
class_data_bits_t类型
4. 获取
class_rw_t *
5. 获取属性列表
因为
list_array_tt内部是一个迭代器实现的,所以使用get()方法即可获取对应的属性,DXJTeacher只有一个属性,取get(1)就越界了
成员变量 类的属性放在ro的成员列表中
- 获取
ro地址
2.获取成员变量ivars地址
3. 解析数据
实例方法 类的属性放在rw的属性列表中
- 获取方法列表,并解析
这里有写的实例方法,以及系统自动生成的hobby的getter setter方法,但是没有类方法。
类方法 类方法放在元类的方法列表中
- 获取元类地址
- 偏移0x20
- 将地址转为
class_data_bits_t类型
- 获取元类方法列表