iOS开发 — 类的缓存分析

540 阅读4分钟

上一篇文章中,我们分析了类的结构和属性、成员变量、实例方法、类方法在类中的存储,那么还剩下一个结构没有分析,就是cache_t,这次就让我们来分析一下。

cache_t的结构

我们先来看一下cache_t的源码:

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结构的源码:

truct 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);
};

bucket_t的源码我们可以猜测,cache_t中缓存的是方法实现imp。

方法缓存流程分析

我们通过在源码中搜索,可以找到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;
    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);
}

这个方法有些复杂,解读之后的具体流程为:

1.通过 cache_getImp 来判断当前 cls 下的 sel 是否已经被缓存了,如果是,直接返回。而 cache_getImp 底层实现是 _cache_getImp,并且是在汇编层实现的。
2.调用 getCache 来获取 cls 的方法缓存,然后通过 getKey 来获取到缓存的 key,这里的 getKey 其实是将 SEL 类型强转成 cache_key_t 类型。
3.在 cache 已经占用的基础上进行加 1,得到的是新的缓存占用大小 newOccupied。
4.然后读取现在缓存的容量 capacity。
5.如果缓存为空了,那么就重新申请一下内存并覆盖之前的缓存,之所以这样做是因为缓存是只读的。
6.如果新的缓存占用大小 小于等于 缓存容量的四分之三,则可以进行缓存流程
7.如果缓存不为空,且缓存占用大小已经超过了容量的四分之三,则需要进行扩容。
8.通过前面生成的 key 在缓存中查找对应的 bucket_t,也就是对应的方法实现。
9.判断获取到的桶 bucket 是否是新的桶,如果是的话,就在缓存里面增加一个占用大小。然后把 key 和 imp 放到桶里面。

我们再来看一下这个方法中其他方法的实现:

#Mark:当前cache是空的话 进行一步创建调用reallocate方法
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
{
    bool freeOld = canBeFreed();
    // 获取旧的 buckets
    bucket_t *oldBuckets = buckets();
    // 创建新的 buckets
    bucket_t *newBuckets = allocateBuckets(newCapacity);

    // Cache’s old contents are not propagated. 
    // This is thought to save cache memory at the cost of extra cache fills.
    // fixme re-measure this

    assert(newCapacity > 0);
    assert((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);
//    设置buckets mask
    setBucketsAndMask(newBuckets, newCapacity - 1);
//    释放旧的buckets 的内存
    if (freeOld) {
        cache_collect_free(oldBuckets, oldCapacity);
        cache_collect(false);
    }
}
#Mark:缓存扩容算法
void cache_t::expand()
{
    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);
}
#Mark:cache->find 方法查找bucket
bucket_t * cache_t::find(cache_key_t k, id receiver)
{
    assert(k != 0);

    bucket_t *b = buckets();
    mask_t m = mask();
// 通过cache_hash函数 begin[k,m]计算key值k 对应的一个index的begin值,用来记录查询的起始索引
    mask_t begin = cache_hash(k, m);
// 将begin赋值给i,用于切换索引值
    mask_t i = begin;
    do {
/**
 用这个i从散列表取值,如果取出来的bucket_t的key=k,则查询成功,返回bucket_t,
 如果key=0,说明索引i的位置上还没有缓存过方法,同样需要返回当前的bucket_t,用于终止缓存查询。
 */
        if (b[i].key() == 0  ||  b[i].key() == k) {
            
            return &b[i];//返回的是地址;
        }
    } while ((i = cache_next(i, m)) != begin);
//这一步相当于i=i-1,回到上面的do循环里面,相当于查找散列表上一个单元格里面的元素,再次进行key值k的比较。
    // hack
    Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
    cache_t::bad_cache(receiver, (SEL)k, cls);
}

本篇文章大致梳理了方法在类中的缓存流程,还有很多细节没有补充,之后有时间会尽量补上。