likes
comments
collection
share

iOS八股文(五)class类结构cache_t源码详解

作者站长头像
站长
· 阅读数 29

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方法来添加的。

iOS八股文(五)class类结构cache_t源码详解

insert方法分为两部分,一部分是真正的插入,另一部分是hash扩容。我们先看真正的插入实现。

insert的插入

源码提示:

fastpath:大概率执行

slowpath:小概率执行

先看源码:

iOS八股文(五)class类结构cache_t源码详解

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

iOS八股文(五)class类结构cache_t源码详解

iOS八股文(五)class类结构cache_t源码详解

iOS八股文(五)class类结构cache_t源码详解

其中cache_hash是hash计算index的函数。通过方法名和m计算出index。 可以简单看一看,不做重点,不理解也不妨碍对整体的理解。

iOS八股文(五)class类结构cache_t源码详解 最关键的代码是对两者做了与运算,其中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的执行,详细注释如下。

iOS八股文(五)class类结构cache_t源码详解 其中,如果hash冲突了,会通过cache_next方法寻找下一个索引i。

在阅读的时候要注意两个容易混淆的概念:

capacity :容量,容纳能力 occupied :实际占用的容量 例如,一个10L的桶子,装了5L水,那么capacity = 10,occupied = 5.

insert的扩容

在插入之前,有对backet的容量判断,如果不够的话将会对其扩容。先看源码;

iOS八股文(五)class类结构cache_t源码详解 以上代码分为4个分支3个判断,我在代码上面写了简单易懂的注释。

iOS八股文(五)class类结构cache_t源码详解 其中 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

方法调用如下:

iOS八股文(五)class类结构cache_t源码详解 打印结果如下(arm64 模拟器环境):

iOS八股文(五)class类结构cache_t源码详解

iOS八股文(五)class类结构cache_t源码详解

iOS八股文(五)class类结构cache_t源码详解

可以看到,分别在method1(4/4),method8(8/8),method22(14/16)的时候进行了扩容。这恰好印证了我们之前得到的结论。注意环境为arm64 模拟器。

思考

在扩容的时候,苹果为什么要释放旧的缓存,而不是把旧的放入到新的缓存中呢?

  1. 提高msgSend效率,扩容是发生在msgSend中,如果再做copy操作,会影响消息发送的效率。
  2. 缓存命中概率,每个方法调用的概率在底层设计的时候,都视为是一样的。所以之前缓存的方法,在后面调用的概率和其他方法的概率是一样的。即清除之前的缓存,不会影响命中概率。
  3. 减少扩容次数,从而提高效率。还是2的衍生,如果及时清除,可以缓存更多的方法,这样,扩容的概率跟放入新缓存相比更小。
转载自:https://juejin.cn/post/7091212924106047495
评论
请登录