美文网首页
系统底层源码分析(17)——类结构中的cache

系统底层源码分析(17)——类结构中的cache

作者: 无悔zero | 来源:发表于2021-06-23 16:17 被阅读0次

    上篇文章探究了类的结构,其中提到cache,今天就来探究一下。

    • 结构
    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
        ...
    };
    
    struct cache_t {
        struct bucket_t *_buckets;//缓存方法
        mask_t _mask;//缓存容量
        mask_t _occupied;//缓存个数
        ...
    };
    
    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__
        MethodCacheIMP _imp;
        cache_key_t _key;
    #else
        cache_key_t _key;
        MethodCacheIMP _imp;
    #endif
    ...
    };
    
    • 作用
    1. 从结构可以看出cache作用应该是调用方法后对其缓存,加快之后的调用速度。我们可以写段代码,检验一下:
    Person *person = [Person alloc];
    Class pClass = [Person class];
    [person sayHello];
    

    接着断点运行打印内存结构:

    2019-12-25 00:39:22.566292+0800 Test[3586:42169] Person say : -[Person sayHello]
    (lldb) x/4gx pClass
    0x1000012e0: 0x001d8001000012b9 0x0000000100b36140
    0x1000012f0: 0x0000000101e23c20 0x0000000100000003
    (lldb) p (cache_t *)0x1000012f0
    (cache_t *) $1 = 0x00000001000012f0
    (lldb) p *$1
    (cache_t) $2 = {
      _buckets = 0x0000000101e23c20
      _mask = 3
      _occupied = 1
    }
    (lldb) p $2._buckets
    (bucket_t *) $3 = 0x0000000101e23c20
    (lldb) p *$3
    (bucket_t) $4 = {
      _key = 4294971020
      _imp = 0x0000000100000c60 (Test`-[Person sayHello] at Person.m:13)
    }
    
    1. 一开始cache没有值,调用[person sayHello]后才有了值,由此可见,类的cache缓存了调用过的实例方法。从而也可以推导出元类的cache缓存了调用过的类方法:
    (lldb) p/x 0x001d8001000012b9 & 0x00007ffffffffff8ULL
    (unsigned long long) $5 = 0x00000001000012b8
    
    (lldb) x/4gx 0x00000001000012b8
    0x1000012b8: 0x001d800100b360f1 0x0000000100b360f0
    0x1000012c8: 0x0000000101e236c0 0x0000000200000003
    (lldb) p (cache_t *)0x1000012c8
    (cache_t *) $6 = 0x00000001000012c8
    (lldb) p *$6
    (cache_t) $7 = {
      _buckets = 0x0000000101e236c0
      _mask = 3
      _occupied = 2
    }
    (lldb) p $7._buckets
    (bucket_t *) $8 = 0x0000000101e236c0
    (lldb) p *$8
    (bucket_t) $9 = {
      _key = 4298994200
      _imp = 0x00000001003cc3b0 (libobjc.A.dylib`::+[NSObject alloc]() at NSObject.mm:2294)
    }
    
    1. 我们继续验证:
    Person *person = [[Person alloc] init];
    Class pClass = [Person class];
    
    [person sayHello];
    [person sayCode];
    [person sayNB]; 
    

    接着断点运行打印内存结构:

    (lldb) x/4gx pClass
    0x1000012e8: 0x001d8001000012c1 0x0000000100b36140
    0x1000012f8: 0x0000000101029950 0x0000000100000007
    (lldb) p (cache_t *)0x1000012f8
    (cache_t *) $1 = 0x00000001000012f8
    (lldb) p *$1
    (cache_t) $2 = {
      _buckets = 0x0000000101029950
      _mask = 7
      _occupied = 1
    }
    (lldb) p $2._buckets
    (bucket_t *) $3 = 0x0000000101029950
    (lldb) p *$3
    (bucket_t) $4 = {
      _key = 0
      _imp = 0x0000000000000000
    }
    (lldb) p $2._buckets[0]
    (bucket_t) $5 = {
      _key = 0
      _imp = 0x0000000000000000
    }
    (lldb) p $2._buckets[1]
    (bucket_t) $6 = {
      _key = 0
      _imp = 0x0000000000000000
    }
    (lldb) p $2._buckets[2]
    (bucket_t) $7 = {
      _key = 4294971026
      _imp = 0x0000000100000ce0 (Test`-[Person sayNB] at Person.m:25)
    }
    (lldb) p $2._buckets[3]
    (bucket_t) $8 = {
      _key = 0
      _imp = 0x0000000000000000
    }
    

    测试调用多个方法时,我们发现缓存的方法只有[Person sayNB],而且_mask3变成了7,这些看来是缓存策略影响了。

    • 源码
    1. 我们从objc4-750源码探究,直入主题,从cache_fill_nolock函数开始。如果已缓存,就获取返回:
    static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
    {
        cacheUpdateLock.assertLocked();
    
        // Never cache before +initialize is done
        if (!cls->isInitialized()) return;
    
        // Make sure the entry wasn't added to the cache by some other thread 
        // before we grabbed the cacheUpdateLock.
        if (cache_getImp(cls, sel)) return;//如果已缓存,就获取返回
    
        cache_t *cache = getCache(cls);
        cache_key_t key = getKey(sel);
    
        // Use the cache as-is if it is less than 3/4 full
        mask_t newOccupied = cache->occupied() + 1;//默认0,递增
        mask_t capacity = cache->capacity();
        if (cache->isConstantEmptyCache()) {//判断缓存容器是否为空
            // Cache is read-only. Replace it.
            cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);//为空就创建
        }
        else if (newOccupied <= capacity / 4 * 3) {
            // Cache is less than 3/4 full. Use it as-is.
        }
        else {
            // Cache is too full. Expand it.
            cache->expand();//超过3/4就扩容
        }
    
        // Scan for the first unused slot and insert there.
        // There is guaranteed to be an empty slot because the 
        // minimum size is 4 and we resized at 3/4 full.
        bucket_t *bucket = cache->find(key, receiver);
        if (bucket->key() == 0) cache->incrementOccupied();
        bucket->set(key, imp);//缓存方法
    }
    
    1. 第一次调用方法([person init]),还没缓存,就调用reallocate,这时capacity为0,INIT_CACHE_SIZE为4,最终_mask会等于3
    struct bucket_t *cache_t::buckets() 
    {
        return _buckets; 
    }
    
    mask_t cache_t::mask() 
    {
        return _mask; 
    }
    
    mask_t cache_t::occupied() 
    {
        return _occupied;
    }
    ...
    mask_t cache_t::capacity() 
    {
        return mask() ? mask()+1 : 0; //默认0,有值+1
    }
    
    enum {
        INIT_CACHE_SIZE_LOG2 = 2,
        INIT_CACHE_SIZE      = (1 << INIT_CACHE_SIZE_LOG2)  //等于4
    };
    
    void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
    {
        bool freeOld = canBeFreed();
    
        bucket_t *oldBuckets = buckets();
        bucket_t *newBuckets = allocateBuckets(newCapacity);//创建
        ...
        setBucketsAndMask(newBuckets, newCapacity - 1);// -1 是一种算法,为了提前扩容,更安全
        
        if (freeOld) {
            cache_collect_free(oldBuckets, oldCapacity);//清空旧的缓存, 所以扩容缓存后,旧的缓存没了
            cache_collect(false);
        }
    }
    
    bucket_t *allocateBuckets(mask_t newCapacity)
    {
        bucket_t *newBuckets = (bucket_t *)
            calloc(cache_t::bytesForCapacity(newCapacity), 1);//初始化
        ...
        return newBuckets;
    }
    
    void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask)
    {
        ...
        _buckets = newBuckets;//新的容器
        ...
        _mask = newMask;
        _occupied = 0;//归0
    }
    
    1. 经过调多个方法后(最后调用[person sayNB]),_mask经过mask()+1newCapacity - 1,此时应为4,缓存空间超过容量的3/4(4 > 4*3/4),需要进行扩容:
    void cache_t::expand()
    {
        cacheUpdateLock.assertLocked();
        
        uint32_t oldCapacity = capacity();
        uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;//变成2倍
    
        if ((uint32_t)(mask_t)newCapacity != newCapacity) {
            // mask overflow - can't grow further
            // fixme this wastes one bit of mask
            newCapacity = oldCapacity;
        }
    
        reallocate(oldCapacity, newCapacity);//重置缓存大小
    }
    

    此时经过reallocate重置缓存大小并清空旧的缓存,所以只保留了[person sayNB],而且_mask变成2倍为8,再通过newCapacity - 1变成7

    1. 缓存容量调整好后,方法最终都会通过bucket->set(key, imp)缓存下来,方法数量也会通过incrementOccupied记录:
    void cache_t::incrementOccupied() 
    {
        _occupied++;
    }
    
    void bucket_t::set(cache_key_t newKey, IMP newImp)
    {
        assert(_key == 0  ||  _key == newKey);
        
        _imp = newImp;
        
        if (_key != newKey) {
            mega_barrier();
            _key = newKey;
        }
    }
    
    • 总结
    • 方法缓存是为了提高程序的执行效率;
    • 类的cache用来缓存实例方法;
    • 元类的cache用来缓存类方法;
    • 如果已有缓存就获取返回;如果没有缓存就会创建容器缓存;如果缓存超出容量的3/4就会扩容,变成2倍,并且清空旧的缓存;

    相关文章

      网友评论

          本文标题:系统底层源码分析(17)——类结构中的cache

          本文链接:https://www.haomeiwen.com/subject/rvgyyltx.html