前言
之前通过内存偏移拿到了objc_class里的bits数据,其中包含了我们属性和方法等一些数据。想要继续探索类的底层结构,就要继续探索cache,这篇文章就来了解下cache_t。
Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits;
cache_t
先来看看cache_t的结构
这里只截取了部分代码,代码很多,还有一些其他以及一些关于MASK的静态数据成员。
第一张图就是cache_t的主要成员变量,就看这几行代码,我们也很难发现cache_t里到底是存的什么,存在哪个成员里。既然不知道,那就继续看代码。
看了后面的代码,我们还发现其他结构体。
bucket_t、objc_imp_cache_entry
分别贴出这两个结构体的代码
通过对比代码,可以发现
bucket_t明显比objc_imp_cache_entry代码更丰富,而且bucket_t使用的频次更高,objc_imp_cache_entry只用了一次,而且还是作为一个objc_imp_cache_entry *buffer这样的缓冲器的变量。所以可以大胆猜测bucket_t就是存储缓存的结构体,并且通过代码,也可以看出主要存储的sel和imp,也就是方法相关的数据。
通过LLDB调试拿到数据
// 先创建一个类
@interface DDAnimal : NSObject
@property (nonatomic, copy) NSString *age;
- (void)jump1;
- (void)jump2;
- (void)jump3;
- (void)jump4;
- (void)jump5;
- (void)jump6;
+ (void)run;
@end
开始lldb调试
// 因为是在类信息里,所以打印类的地址
(lldb) p/x DDAnimal.class
(Class) $0 = 0x0000000100008a58 DDAnimal
// 因为cache之前只有isa和superClass,都是8字节,所以平移16个字节
(lldb) p/x (cache_t *)0x0000000100008a58 + 0x10
(cache_t *) $1 = 0x0000000100008a68
(lldb) p/x $1->buckets()
(bucket_t *) $2 = 0x00000001007174e0
// buckets()看名字应该是数组,那么直接用下标取值,同时在bucket_t里找到获取sel的方法
(lldb) p/x $2[0].sel()
(SEL) $15 = 0x0000000100003cb4 "jump2"
(lldb) p/x $2[1].sel()
(SEL) $16 = 0x0000000000000000 <no value available>
(lldb) p/x $2[2].sel()
(SEL) $17 = 0x0000000100003cae "jump1"
// 同样的可以通过 inline IMP imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls) 获取imp
(lldb) p/x $7->imp(nil, DDAnimal.class)
(IMP) $27 = 0x0000000100003ab0 (KCObjcBuild-[DDAnimal jump1])
// 看看还能获取什么,在代码里找到这几个方法
(lldb) p/x $1->cls()
(Class) $29 = 0x0000000100008a58 DDAnimal
(lldb) p/x $1->occupied()
(mask_t) $28 = 0x00000002
(lldb) p/x $1->mask()
(mask_t) $30 = 0x00000003
从上面这段调试了解到cache里面存的就是方法的调用缓存,并且是存在_bucketsAndMaybeMask里的(因为buckets()函数里就是直接从_bucketsAndMaybeMask里去取值)。同时还有其他的几个问题,如下:
-
- 缓存的
buckets()究竟有多大,这里只打印了3个,尝试打印了[10]得到的也是空,无法确认开辟了多大的控件。并且打印出来的数据是无序的,是因为buckets是使用哈希表来进行存储的。
- 缓存的
-
occupied:单词意思是已占用,值 = 2,可以看出我们调用了两次方法,占用两个位置。
-
mask:_maybeMask,值 = 3,这个代表什么呢?
接下来就需要对代码进行研究,了解cache_t的工作原理。在此之前,我们再来看一下在非源码情况下调试cache的方法。
Tips: buckets()也可以用指针偏移来获取数据,例如
(lldb) p/x $2+2
(bucket_t *) $7 = 0x0000000100717500
(lldb) p/x $7->sel()
(SEL) $8 = 0x0000000100003cae "jump1"
通过代码调试拿到数据
- 原理:因为都是指针类型,就可以自己写一份结构一样的代码,只要最终数据类型一样,就可以拿到数据。
- 目的:1. 方便调试,lldb一旦调试不对,就要重头再来。2. 通过代码分析,能对底层结构更加清晰。
- 规则:结构体里的成员一一对应,顺序不变,缺一不可(至少前面的不能少,否则后面的数据就不对),环境里没有的结构体就自己创建一个,里面的成员也一一对应,结构体名称最好和源码有点区别,避免冲突。
下面贴出代码,左边是自己写的代码,右边是objc源码。
通过源码可以看到objc_class只有三个成员,直接复制过来,但是因为其继承objc_object,其内有一个isa成员,而我们没有任何继承,所以这里需要手动加上,不然对不上。
由于自己的代码里没有cache_t,所以创建一个。
explicit_atomic<>包装我们用不上,所以直接删除,union里的_originalPreoptCache我们用不上,所以直接删除,那么union也就用不上了,也直接删掉。mask_t就是个uint32_t类型的数据,直接创建一个typedef uint32_t mask_t;。通过上面的lldb调试了解到_bucketsAndMaybeMask里装的就是bucket,所以直接使用bucket结构体。
这个结构体就一个成员。friend关键字:在一个类中指明其他的类(或者)函数能够直接访问该类中的private和protected成员,相当于是引用,所以不是成员,可以直接删掉。
bucket_t里面的成员有一个判断,如果是
arm64走上面,其他的走下面,arm64就是真机,而我们是在模拟器上,所以就用下面的顺序。而_imp就是IMP,所以直接使用IMP了。
代码书写完毕,开始调试,可以看出,对象方法的调用缓存是放在类里面的,类方法的调用缓存是放在元类的里面的,但是也有几个问题。
- 调用了4次对象方法,但是缓存里只有两个,其他的去哪里了?
- 类缓存中的_maybeMask = 7,元类的_maybeMask = 3,这是为什么?
分析源码
上图是cache_t插入sel的流程,从上图的分析中可以看出:
- 当
newOccuipied + 1 > capacity * 3/4时,就会开辟新的空间,并且把旧的buckets删掉。而为什么会在0.75倍的时候进行扩展,是因为在0.75的空间利用率最佳,哈希冲突的概率也会有效的降低。 - capacity初始值为4,每次开辟新的空间按照
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE的规则,并且_maybeMask是按照capacity-1的值来进行存储。
总结
cache_t主要存储了方法的sel和imp,并且按照复杂的运算,本文并未吃透其中的逻辑,想要了解透彻,还需要对代码有更多的理解。