前言
什么是cache,缓存,缓存什么?怎么缓存?cache的结构是什么?缓存流程?带着这些疑问开启本文。
准备工作
- objc4-818.2 源码
cache_t 结构
private:
explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
union {
struct {
explicit_atomic<mask_t> _maybeMask;
#if __LP64__
uint16_t _flags;
#endif
uint16_t _occupied;
};
explicit_atomic<preopt_cache_t *> _originalPreoptCache;
};
void incrementOccupied();
void setBucketsAndMask(**struct** bucket_t *newBuckets, mask_t newMask);
void reallocate(mask_t oldCapacity, mask_t newCapacity, **bool** freeOld);
void collect_free(bucket_t *oldBuckets, mask_t oldCapacity);
static bucket_t *emptyBuckets();
static bucket_t *allocateBuckets(mask_t newCapacity);
static bucket_t *emptyBucketsForCapacity(mask_t capacity, bool allocate = true);
struct bucket_t *buckets() const;
static struct bucket_t * endMarker(**struct** bucket_t *b, uint32_t cap);
根据这个结构我们大概可以看出来buckets很重要,emptyBuckets,struct bucket_t *buckets(),static bucket_t *allocateBuckets(mask_t newCapacity)都可以看出来在操作buckets.我们走进去看下结构。
bucket_t的结构
struct bucket_t {
private:
#if __arm64__
explicit_atomic<uintptr_t> _imp;
explicit_atomic<SEL> _sel;
#else
explicit_atomic<SEL> _sel;
explicit_atomic<uintptr_t> _imp;
}
sel,imp 这不就是方法编号和函数指针地址吗!原来bucket里面存放了sel和imp.
cache 缓存的是方法
bucket_t的结构图解
lldb调试
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *p = [LGPerson alloc];
Class pClass = [LGPerson **class**];
NSLog(@"%@",pClass);
}
return 0;
}
-
类的首地址平移
16字节得到cache -
输出cache没发现我们想要的数据
-
lldb打印方法buckets()
- lldb 调用
p [p saySomething]方法 - 在一次打印cache 里面发现了
_maybeMask value = 7 _occupied =3 - _
bucketsAndMaybeMask存放了buckets的首地址方便取bucket
-
lldb 调用
p [p saySomething]方法后_maybeMask value = 7 _occupied =3但不清楚这些参数的作用 -
bucket_t中提供了相应获取imp 和sel的方法
-
saySomething方法的sel,imp 存放在bucket中 buckets中有很多个bucket
总结
通过lldb调试,结合源码。cache中存的是方法,方法的sel和imp存在bucket。buckets中存放了很多个bucket。
cache中的bucketsAndMaybeMask 存放了buckets的首地址,方便平移去获取bucket。
cache_t 脱离源码环境分析
说明:很多情况下我们下载下来的源码很多情况都是不能调试的,通过lldb调试也需要在源码环境,而且不方便,错一个步骤就要重新来过。下面这种方式是一种全新思路,方便我们快速去调试,局部抽取相应需要的结构来达到我们的需求。
抽取如图
脱离源码环境打印分析
我们会产生下面几个疑问
-
问题:1
occupied在调用3个方法时变为1,maybeMask变为7? -
问题:2
say1 say2方法不见了? -
问题:3
cache存储的位置怎么是乱序的呢?比如say2在say1前面,sayHello3前面的位置是空的
带着这些疑问继续探讨cache_t,_occupied和_maybeMask是什么?。我们要缓存方法,首先就要是怎么把方法插入到buket中的。所以我们带着这个思路走进 cache_t源码中
cache_t 源码 insert方法
void insert(SEL sel, IMP imp, id receiver);
- 操作了
occupied+1 第一次进来occupied=0 occupied目的是为了记录类中有多少缓存方法,每进来一次加一个,也就是buckets中bucket的数量oldCapacity为了记录之前开辟的容量 第一次进来等于0方面后面扩容清理
扩容流程
- 第一次进入判断是否是
空缓存是的话reallocate开辟容量4 - 每缓存一个方法
newOccupied+1直到缓存的容量大于开辟的4/3,进行两倍扩容也就是capacity=8
reallocate 方法分析
allocateBuckets开辟桶子内存- 进行
capacity减一操作来控制边界 collect_free是否释放旧的内存,由freeOld控制
setBucketsAndMask方法分析
setBucketsAndMask主要根据不同的架构系统向_bucketsAndMaybeMask 和 _maybeMask写入数据
缓存方法分析
-
首先拿到
bucket()指向开辟这块内存首地址,也就是第一个bucket的地址,bucket()既不是数组也不是链表,只是一块连续的内存 -
hash函数根据缓存sel和mask,计算出hash下标。为什么需要mask呢?mask的实际作用是告诉系统你只能存前capacity - 1中的位置,比如capacity = 4时,缓存的方法只能存前面3个空位 -
开始缓存,当前的位置没有数据,就缓存该方法。如果该位置有方法且和你的方法一样的,说明该方法缓存过了,直接
return。如果存在hash冲突,下标一样,sel不一样,此时会进行再次hash,冲突解决继续缓存
cache_hash 和 cache_next
static** inline mask_t cache_hash(SEL sel, mask_t mask)
{
uintptr_t value = (uintptr_t)sel;
#if CONFIG_USE_PREOPT_CACHES
value ^= value >> 7;
#endif
return (mask_t)(value & mask);
}
__arm64__
static inline mask_t cache_next(mask_t i, mask_t mask) {
return i ? i-1 : mask;
}
cache_hash主要是生成hash下标,cache_next主要是解决hash冲突
insert调用流程
insert的我们前面内容可以看出来它的作用是缓存方法的入口,那是什么样的条件会调期insert的方法了,首先在insert方法中打个断点。运行源码看下。
- 上图的堆栈信息显示insert的流程
_objc_msgSend_uncached ->lookUpImpOrForward -> log_and_fill_cache -> cache_t::insert
在来看一下汇编流程
调用insert方法流程:[p saySomething]底层实现 objc_msgSend --> _objc_msgSend_uncached --> lookUpImpOrForward --> log_and_fill_cache --> cache_t::insert
insert流程图
方法调用缓存流程图
总结
cache_t 中各个变量的含义
_bucketsAndMaybeMask存储buckets和msak(真机),macOS或者模拟器存储buckets是buckets的首地址,方便找bucket_maybeMask是指掩码数据,用于在哈希算法或者哈希冲突算法中哈希下标_maybeMask=capacity -1_occupied会随着缓存的个数增加,扩容是_occupied=0- 数据丢失是因为
扩容的时候旧的内存回收了数据全部清除 cache存储bucket的位置乱序,因为位置是hash根据你的sel和mask生成所以不固定