前言
我们之前介绍过类的结构(类的结构传送门),重点介绍了class_data_bits_t的内部结构。这张我们着重介绍下类结构里的另一个重要的属性cache_t。
初探cache_t
我们先准备环境,创建Person类,在.h,.m,main.m写如下代码
@interface Person : NSObject
- (void)likeFood;
- (void)homeAddress;
- (void)toWorkTime;
- (void)toSleepTime;
@end
@implementation Person
- (void)likeFood {
NSLog(@"Person say : %s",__func__);
}
- (void)homeAddress {
NSLog(@"Person say : %s",__func__);
}
- (void)toWorkTime {
NSLog(@"Person say : %s",__func__);
}
- (void)toSleepTime {
NSLog(@"Person say : %s",__func__);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
Person *p = [Person alloc];
Class pClass = [Person class];
[p likeFood];
[p homeAddress];
[p toSleepTime];
[p toWorkTime];
}
return 0;
}
运行打断点,我们知道在获取class_data_bits_t时用了内存平移,这次获取cache_t也应该类似,我们试一下。
我们看到断点还没有执行方法,此时打印的cache_t如上图所示,注意红框内容,此时为0;
我们过一下断点
我们发现红框的内容
_mask由之前的0变为了3,_occupied有原来的0变为了1。这中间的变化就是执行了一个方法。我们继续过断点
我们发现
_mask还是3并未变化,_occupied有原来的1变为了2。这中间的变化就是又执行了一个方法。我们继续过断点
我们发现
_mask由之前的3变为了7,_occupied有原来的2变为了1。这中间的变化就是又执行了一个方法。继续过断点
我们发现
_mask还是7并未变化,_occupied有原来的1变为了2。这中间的变化就是又执行了一个方法。
我们发现cache_t的变化跟执行方法有关,因为cache_t是做缓存的,那就可能里面存了执行过的方法。至于_mask跟_occupied的变化规律,我们通过源码来分析。
了解cache_t内部结构
我们看下源码的cache_t内部结构是什么样的,里面代码太多了,只贴重要的信息
- CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED这个指如果是Mac,
- CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16指是真机
- CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4指模拟器
我们再去看看bucket_t里面都是什么,贴重要的
explicit_atomic是原子性操作,因为是缓存,所以保证安全
可以看到里面有_imp和_sel其中#if__arm64__如果是真机的意思
我们发现这里面没有我们打印出现的_occupied,那它在哪呢?我们全局搜一下
看了下好像都不对,我们再搜下occupied,我们找打如下的方法
insert是加入的意思,那这个方法是不是向cache_t里插入方法的方法呢?我们打断点验证下。
探究cache_t的插入过程
运行代码,我们发现代码进来了,我们先总览一下这个方法
下面我们解释下方法
- mask_t newOccupied = occupied() + 1;
occupied()调用方法,返回的是_occupied,初始值为0,也就是新来一个方法,newOccupied就等于1,来一个方法就+1,它的值相当与当前存了多少方法
- unsigned oldCapacity = capacity(), capacity = oldCapacity;
先取旧值capacity()返回的是上次的内存空间大小,让capacity等于上次的空间大小 - if (slowpath(isConstantEmptyCache())) {就是判断当前的cache是否有值,如果没有进入,我们第一次调方法肯定没有值。
- if (!capacity) capacity = INIT_CACHE_SIZE;我们第一次调用capacity肯定为空,所以给capacity赋个零时值为4,
INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2),是1左移2位,就是4. - reallocate(oldCapacity, capacity, /* freeOld */false);reallocate方法内部是初始化cache_t,我们这里是开辟4个空间大小的cache_t
- else if (fastpath(newOccupied + CACHE_END_MARKER <= capacity / 4*3)) {如果
newOccupied+1小于等于开辟空间大小的3/4时,就进入,否则就进入下面。我们这里是进去, - bucket_t *b = buckets();当前的cache_t的sel以及imp对应的哈希表给b
- mask_t m = capacity - 1;是当前的m等于开辟的内存-1.
- mask_t begin = cache_hash(sel, m);是拿
当前的m跟方法名通过哈希算法得到下标,cache_hash方法:return (mask_t)(uintptr_t)sel & mask;就是方法名跟当前内存大小取与 - 后面集中说下,把当前方法要存的下标给i,进入循环,
这个bucket_t集合取下标i的sel,如果不存在直接将方法名sel以及imp存在bucket_t,跳出循环,如果等于sel则返回结束,如果存在切不等于,那就将算的下标+1在进行哈希运算,得到新的下标,继续比,知道不存在,存入返回结束
我们刚有个判断,如果小于等于开辟空间大小的3/4,就到下面进行存,如果大于会走下面的方法
- capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;是
如果大于开辟内存的3/4,capacity如果存在就开辟之前空间的2倍,如果不存在就开辟4个字节大小
- 后面是给capacity一个最大值,如果大于最大值就取最大值,最大值是1左移16位
- 开辟空间,
但跟之前的cache_t为空不同,它会走cache_collect_free方法,这方法是把老的内存方法存到garbage_refs里
上面就是方法调用时如何插入到cache_t中。实际操作发现,如果cache_t存在该方法,再次调用该方法,是不走cache_t::insert方法的
补充内容
后面我有研究了cache_t的方法,现将研究内容写一下,不对的地方希望大家指正。
cache_t判断
isConstantEmptyCache方法理解
第653行,这个判断通过下面的注释我们知道
如果cache_t是只读属性,就替换成新的。原因也很简单,因为下面要进行方法写入,这里的cache_t只读明显是不行的。我们需要去看看这个判空是怎么操作的。我们进入方法
之前说了occupied()如果是新的,那么默认值为0,如果不是新的(里面有缓存)那么就不为0,所以如果cache_t不是新的,就返回false。
但是我们还是去看下emptyBucketsForCapacity方法
- 518-522行:就是上锁
- 524行:获取当前cache_t的大小
- 527-529行:大小小于EMPTY_BYTES直接返回新的Buckets(cache_t就是bucket的集合)
- 走到532行说明,大小不小于EMPTY_BYTES此时就是在堆区分配一个buckets。533行就是设置默认值为0。
- 535行就是让index经过一系列的计算(里面是个指数函数)
- 537-554行就是初始化一个等大的空间,将newBuckets存储进emptyBucketsList,把中间空的数组填满,便于hash key 落在之间的对象获取bucket_t数组。
- 返回emptyBucketsList下标为1的元素。
上面判断在653行如果不是新的cache_t,基本上是不会走到655行的。
cache_collect方法理解
上面讲到,如果调用了reallocate方法,oldCapacity旧值存在(不是新值)就会调用cache_collect_free方法,下面我们看下cache_collect_free方法,下图
- 981-984行:还是加锁
- 986行:PrintCaches是否开启方法写入
- 988行:就是对garbage进行扩容
- 989行:就是将老的缓存大小记录下来(累加到garbage_byte_size)
- 990行:就是将缓存值存到garbage_refs中
- 991行:调用cache_collect传入false
下面我们看下cache_collect方法
说明:折叠的方法是不用看的,PrintCaches是是否开启日志打印,我们没开启,所以不用看(1023,1046行)。1023行,因为我们刚传的是false,所以判断会走1013行
- 1001-1005行:上锁
- 1008-1010行:就是判断garbage是不是满了,如果未满且传值为false,直接返回。如果满了就继续走
- 1012-1022行:如果为false,就判断是否有在查询缓存,如果有就返回,没有继续
- 1037-1041行:清除garbage将garbage存的数据释放
- 1047-1048行:都置为0.
什么时候会清garbage,
发现只有刷新所有方法缓存才会清。目前只有调用instrumentObjcMessageSends置为YES。以上就补充内容
下面我们运行去打印cache_t的内容
打印cache_t
打印过程及分析
我之前文章写过如何打印class_data_bits_t(传送门),打印cache_t也是一样的方法
我们运行代码,打断点
因为cache_t在class中是偏移了16字节,故需在class首地址偏移16
我们打印*1地址下的信息,
按着class_data_bits_t,类推,cache_t里包含_buckets,_mask(因为实在mac上运行),
而_buckets里面又包含imp,sel,打印应该如下
我们发现不对,这是为什么?
我们在找class_data_bits_t,先调用data()方法获取都bits.data(),返回的是class_rw_t类型的对象,接着我们调用的获取属性方法properties(),获取方法列表方法methods(),而不是直接拿的属性。那cache_t有没有这些方法呢?我们查看cache_t源码
上图红框所示,获取bucket_t的方法,第一个是返回empty的Buckets。知道这些,我们在打印
在打印sel的时候又不对了,我们再去bucket_t方法里寻找,发现下图
sel()是获取SEL方法,imp(Class cls)是获取imp方法,所以我们._sel是不对的。我们按方法打印
打印出来我们想要的方法了。
imp(pClass),是因为imp是需要跟class参数的。
通过上面的打印我们总结如下
- 结构体属性的指针打印,可以通过偏移量进行
- 获取属性,优先使用方法,查看整个方法,看看有没有返回该属性的方法,有的话优先使用
- *1地址N内存储的内容,它的内容包含这块内容的指针地址,需要拿地址才能获取到它的内容
如何打印下个方法呢?上面我们分析cache_t的存入,方法是转成bucket_t类型通过哈希算法存到cache_t的,所以他们下标是不连续的,是根据哈希算法得到的值,我们上面已经获取到一个likeFood的bucket_t,那么homeAddress的bucket_t是不是在下一位,试一下
拿到homeAddress了
上面我们探究的是_mask为3,_occupied为2的情况,我们看下_mask为7,_occupied为2的情况,打印出现有意思的情况,打印比较长
第一次打印什么都没有,该bucket_t为空,不存在值
打印有值,把toWorkTime打印出来了。,继续下一位
打印也是什么都没有,我们的toSleepTime还没找到,继续平移下一位
依然什么都没有,继续下一位
这次打印,我们找到了toSleepTime,我们继续打印完,看看有没有我们前面调用的likeFood方法。
到此打印完了,我们移动了7次,内存打印完,并没有发现之前调用的likeFood以及homeAddress方法。总结如下:
- 方法位置是无序的,是根据哈希算法算的位置,所以方法没有前后之分,算的是什么位置,就在什么位置。
- 内存不够的时候,开辟新内存,会将原来的存储内容清空
上面讲cache_t的存储过程,打印也印证了我们的分析。
最后
传一张cache_t的流程图
cache_t的过程分析完了