美文网首页
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