美文网首页
OC底层cache_t小结

OC底层cache_t小结

作者: 三国韩信 | 来源:发表于2020-06-11 18:46 被阅读0次

    众所周知oc的类的底层是一个结构体,如下。

    struct objc_class : objc_object {
        // Class ISA;
        Class superclass;
        cache_t cache;           
        class_data_bits_t bits; 
        .......
    }
    

    该结构体包含了4个成员变量:isa、superclass、cache、bits。其中isa是指向元类的、superclass是指向父类的、bits是通过bits.data()函数来获取rw,从而能够得到类的其他信息,比如ivar、property、method、protocol等信息。
    那么cache是用来做啥的呢? ——使用来缓存对象调用过的方法的,下次调用方法时就可以直接在这里找到了。

    首先来看一下cache这个结构体的内容是啥:

    struct cache_t {
        struct bucket_t *_buckets;
        mask_t _mask;
        mask_t _occupied;
    
    public:
        struct bucket_t *buckets();
        mask_t mask();
        mask_t occupied();
        void incrementOccupied();
        void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);
        void initializeToEmpty();
    
        mask_t capacity();
        bool isConstantEmptyCache();
        bool canBeFreed();
    
        static size_t bytesForCapacity(uint32_t cap);
        static struct bucket_t * endMarker(struct bucket_t *b, uint32_t cap);
    
        void expand();
        void reallocate(mask_t oldCapacity, mask_t newCapacity);
        struct bucket_t * find(cache_key_t key, id receiver);
    
        static void bad_cache(id receiver, SEL sel, Class isa) __attribute__((noreturn));
    };
    
    // bucket_t的结构体内容
    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
    
    public:
        inline cache_key_t key() const { return _key; }
        inline IMP imp() const { return (IMP)_imp; }
        inline void setKey(cache_key_t newKey) { _key = newKey; }
        inline void setImp(IMP newImp) { _imp = newImp; }
    
        void set(cache_key_t newKey, IMP newImp);
    };
    
    

    cache结构体的成员变量有3个:_buckets、 _mask、_occupied
    通过源码可以很明显的看出来,_buckets是用来缓存调用过的方法的。
    那么cache是怎么缓存方法的呢,有啥缓存策略么?
    通过看cache结构体中的expend函数,我们可以窥探到cache一些扩容的策略

    {
        cacheUpdateLock.assertLocked();
        
        uint32_t oldCapacity = capacity();
        uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;
    
        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);
    }
    

    INIT_CACHE_SIZE:4
    int32_t newCapacity = oldCapacity ? oldCapacity * 2 : INIT_CACHE_SIZE
    通过这2句代码看出cache每次扩容都是在原来的基础上扩容2倍,或者第一次就初始化为容量4。注: 在扩容的时候,会把之前缓存的全部清零。
    那么啥情况下回触发扩容呢?

    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;
        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();
        }
    
        // 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);
    }
    

    通过这个cache_fill_nolock函数我们可以看出,当开始要缓存一个方法时,

    1. 首先会先判断cache中有没有存在这个SEL了,如果能找到,就直接return(if (cache_getImp(cls, sel)) return;
    2. 如果没有,则先判断当前的cache的容量。(newOccupied <= capacity / 4 * 3)通过这句代码可以看出如果超过3/4,就会触发expend函数,就是扩容。
    3. 最后无论是否扩容了,都会把这个最新调用的SEL存到cache里。
      bucket_t * bucket = cache->find(key, receiver);
      if (bucket->key() == 0) cache->incrementOccupied();
      bucket->set(key, imp);

    最后总结cache的调用流程入下图:

    cache流程图.png

    最后的流程图是盗用了网络上某大神的图片,非本人自画,感谢!

    相关文章

      网友评论

          本文标题:OC底层cache_t小结

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