cache_t结构
在objc4源码中,objc_class
结构中有一个cache_t
的成员变量。
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
}
cache
的作用是在objc_msgSend
进程中会先在cache
中依据办法名来hash查找办法完成,假如能查找到就直接掉用。假如查找不到然后再去rw_t
中查找。然后再在cache
中缓存。
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;
};
能够看到有两个成员变量组成。但从界说中看不出成员变量的含义,咱们需求结合其间一个办法的完成去了解。
insert办法
前面提到在objc_msgSend
的时分会在cache
中增加办法缓存,而这个办法缓存是经过insert
办法来增加的。

insert办法分为两部分,一部分是真实的刺进,另一部分是hash扩容。咱们先看真实的刺进完成。
insert的刺进
源码提示:
fastpath:大概率履行
slowpath:小概率履行
先看源码:

b
是办法缓存的桶子(哈希表)的指针;
capacity
是现在桶子现在的总容量,那么m
是桶子现在的容量减1,即桶子最大的索引。



其间cache_hash
是hash核算index的函数。经过办法名和m核算出index。
能够简略看一看,不做重点,不理解也不妨碍对全体的理解。

与运算
,其间mask
肯定是2的n次方减1(低位满是1: 0x11、0x1111这类的),这样相当于是对2的n次方取余了。这样算出的index不会有越界的问题。至于为什么是2的n次方减1,capacity
的扩容都是2倍的,初始化的容量也是1左移1位(arm64)或者2位(x86)的值,m = capacity - 1
,这个mask
就是2的n次方减1了。
剩余是do while
的履行,具体注释如下。

cache_next
办法寻觅下一个索引i。
在阅读的时分要留意两个简略混杂的概念:
capacity :容量,容纳才能
occupied :实际占用的容量 例如,一个10L的桶子,装了5L水,那么capacity = 10,occupied = 5.
insert的扩容
在刺进之前,有对backet的容量判别,假如不够的话将会对其扩容。先看源码;


INIT_CACHE_SIZE
的值在arm64的时分是2,在x86_64的结构下是4。
CACHE_END_MARKER
在arm64下为 0, 在x86_64下为 1。
cache_fill_ratio()
在arm64下为7/8, 在x86_64下位3/4。
总结:
arm64结构下,当现在缓存的巨细+1小于等于桶子的巨细的7/8的时分不扩容,当桶子的巨细小于等于8,并且现在缓存的巨细+1小于等于桶子的巨细的时分也不扩容(桶子小于8的时分存满了才扩容)。
x86_64结构下,当现在缓存的巨细+1,再+1小于等于桶子巨细的3/4的时分不扩容。
代码证实
咱们能够经过c++代码界说class的结构,然后经过桥接的方式来获取cache的结构,从而调查backet里边的缓存情况(代码可留言,邮箱发送)。
@implementation NSObject (OSCacheT)
- (void)printCacheT {
Class cls = [self class];
//将类型转换成自界说的源码os_objc_class类型,便利后续操作
struct os_objc_class *pClass = (__bridge struct os_objc_class *)(cls);
struct os_cache_t cache = pClass->cache;
struct os_bucket_t * buckets = cache.buckets();
struct os_preopt_cache_t origin = cache._originalPreoptCache;
uintptr_t mask = cache.mask();
NSLog(@"桶子里缓存办法的个数 = %u, 桶子的长度 = %lu",origin.occupied,mask+1);
//打印buckets
for (int i = 0; i < mask + 1; i++ ) {
SEL sel = buckets[i]._sel;
IMP imp = buckets[i]._imp;
NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
}
NSLog(@"------\r");
}
@end
办法调用如下:




能够看到,分别在method1
(4/4),method8
(8/8),method22
(14/16)的时分进行了扩容。这恰好印证了咱们之前得到的定论。留意环境为arm64 模拟器。
考虑
在扩容的时分,苹果为什么要释放旧的缓存,而不是把旧的放入到新的缓存中呢?
- 提高
msgSend
功率,扩容是发生在msgSend
中,假如再做copy
操作,会影响消息发送的功率。 - 缓存命中概率,每个办法调用的概率在底层规划的时分,都视为是一样的。所以之前缓存的办法,在后面调用的概率和其他办法的概率是一样的。即铲除之前的缓存,不会影响命中概率。
- 削减扩容次数,从而提高功率。还是2的衍生,假如及时铲除,能够缓存更多的办法,这样,扩容的概率跟放入新缓存相比更小。