在前面的篇章我们探索了类的底层isa走位图
、class_data_bits_t bits
数据,那么这个篇章我们一起探索cache_t cache
类的缓存。带着以下几个问题,一起走进cache探索之旅。
- cache到底是什么,是存什么数据?
- cache的设计有何意义?
- cache_t是什么结构?
- cache是如何缓存数据的?
iOS底层探索--方法慢速查找 这个篇章在方法的慢速查找之后有一部分cache的insert,方法插入缓存段
iOS底层探索--方法快速查找(汇编)流程&慢速查找初探 这个篇章可以从汇编快速的查找到SEL的IMP。
一、cache到底是什么,是存什么数据?
以上这两个篇章的内容就可以解释cache到底是什么,是苹果对于方法调用的一种缓存机制,直接缓存方法的IMP与SEL的对应关系,以哈希表的形式存储起来,对SEL进行哈希,遇到冲突再哈希,将其值做了Key,(SEL+IMP也就是bucket)作为值,缓存起来,存的就是IMP和SEL。
二、cache的设计有何意义?
- 因为方法的调用最后都转变成了消息的发送
objc_msgSend
,会给某个对象发送一条sel的消息,采用一对一的哈希键值可以快速的找到方法的实现。 - 方法的快速查找(汇编通过寄存器查找)可以快速的找到方法的IMP
- 采用缓存就不用每次都去遍历类的方法列表查找方法的实现,当类存在多个分类的时候,效率更加慢。
总结一点:一切都是为了性能、效率。
三、cache_t是什么结构?
struct 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;
};
}
struct bucket_t {
private:
// IMP-first is better for arm64e ptrauth and no worse for arm64.
// SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
explicit_atomic<uintptr_t> _imp;
explicit_atomic<SEL> _sel;
#else
explicit_atomic<SEL> _sel;
explicit_atomic<uintptr_t> _imp;
#endif
![](https://img.haomeiwen.com/i8753265/7c5068f7e1c57064.png)
四、 cache是如何缓存数据的?
这实际上就是要看cache是如何建立起来的,方法是如何插入的:首先查看源码时可能会涉及到memory_order_relaxed
和memory_order_release
等可以查阅这篇文章 内存顺序(Memory Order)
cache的insert流程:
cls->cache.insert(sel, imp, receiver);
- 检测类的初始化标志位,如果没有初始化就什么不干
- 拿到newOccupied,oldCapacity,capacity初始值=oldCapacity
- 判断是否第一次申请缓存空间还是需要扩容
- 判断如果是空缓存,如果是就申请缓存空间
reallocate(oldCapacity, capacity, /* freeOld */false);
创建容量为capacity大小(INIT_CACHE_SIZE,即是1<<2 = 4)的空间- 获取旧的buckets(根据第三个参数为true就进行释放旧值)
- 调用
allocateBuckets(newCapacity);
创建一个capacity(4个)大小容量的buckets容器(以下说明创建的时候最后一个bucket_t就已经被占用了作为一个标记)-
calloc(bytesForCapacity(newCapacity), 1);
创建容器 - 获取到最后一个bucket_t指针end
- 设置最后一个bucket_t的SEL为1,IMP指向容器的首地址,也就是第一个bucket_t
- 检测环境变量OBJC_PRINT_CACHE_SETUP为true,打印缓存容器
-
-
setBucketsAndMask(newBuckets, newCapacity - 1);
设置的时候排除最后一个结束标志位固定占用的大小- _occupied初始化为
0
- _occupied初始化为
- 如果是扩容也就是freeOld=true则
collect_free(oldBuckets, oldCapacity)
- 如果有缓存空间了,新的占用量newOccupied + 1个结束标记bucket <= 缓存填充比例3/4.
- 如果达到扩容条件(也就是大于缓存填充比例),就进行2倍扩容,但是capacity最大不超过2^16=65536,重新调用
reallocate(oldCapacity, capacity, true);
创建更大的容器,并且把旧的bucktes释放,也就是把所有的bucket_t丢弃。(为什么丢弃:因为将旧数据挪到新的buckets的CPU开销成本太大)
- 判断如果是空缓存,如果是就申请缓存空间
- 创建一个新的bucket_t b = buckets(), 对sel进行hash ---> (sel & (capacity-1))得到一个下标
- 进行do...while循环,查找一个未使用的插槽并插入
- 以sel的hash值为key,从bucket中取出sel,
- 如果不存在_occupied++,给bucket set sel和IMP set(b,sel, imp, cls()),设置的时候根据是都原子和encode对IMP进行编码再存储
- 如果sel已经存在,说明已存在缓存,则什么也不干直接退出
- 条件cache_next(i, (capacity - 1)) --> (i + 1) & (capacity - 1) != begin也就是进行在哈希
- 以sel的hash值为key,从bucket中取出sel,
- 如果插入不成功,就bad_cache
网友评论