iOS底层(五)-cache_t分析

487 阅读4分钟

一、cache_t的结构

iOS开发中, 一个类的结构是objc_class. 本文主要分析objc_class中的cache_t这个结构体.

来看一下cacahe_t的代码结构:

#if __LP64__
typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits
#else
typedef uint16_t mask_t;
#endif

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


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

二、cache_t的方法缓存

新建一个测试类:

@interface MyTest : NSObject

@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *gender;

- (void)sayHello;
- (void)sayWorld;
- (void)sayCode;
- (void)sayObject;
@end

在第一次调用sayHello方法前打印cache_t, 发现_msak _occupied为0

调用完第一个sayHello:

调用完一次sayHello方法后发现, _msak _occupied均发生了变化, 并且在_buckets指针里还有相应的方法名. 这就意味着它生成了一个缓存列表.(注: 类方法从元类里找缓存)

调用所有方法:

发现mask变成7, 但是其中的_imp竟然是一个空的. 其中肯定存在问题, 我们需要处理一下. 并不是调用一个方法就缓存一个, 需要特殊处理; 首先_mask发生变化了, 就去找一下mask的方法在哪里调用的:

mask_t mask();

mask_t cache_t::capacity() 
{
    return mask() ? mask()+1 : 0; 
}

跟进调用capacity(),发现这样一个方法里调用了 :

enum {
    INIT_CACHE_SIZE_LOG2 = 2,
    INIT_CACHE_SIZE      = (1 << INIT_CACHE_SIZE_LOG2)
};
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);
}

根据代码得知, 获取了当前容量, 对它进行了一个两倍的扩大. 如果是第一次开辟容量, 则直接容量为4.

来找一下什么时候才会进行扩容.全局搜索 expand():

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;//从缓存里得到imp. 命中缓存直接返回
    
    //没有命中缓存
    cache_t *cache = getCache(cls);//获取这个cls的缓存
    cache_key_t key = getKey(sel);//获取缓存的方法的key

    // Use the cache as-is if it is less than 3/4 full
    mask_t newOccupied = cache->occupied() + 1;//生成occupied(占用量)
    mask_t capacity = cache->capacity();//生成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);//寻址:通过key(哈希值)得到一个地址(方法)
    if (bucket->key() == 0) cache->incrementOccupied();
    bucket->set(key, imp);
}

来进行一下源码跟踪:

2.1、第一次调用方法

获取当前类的缓存. 第一次调用方法, 需要开辟容量, 进入 cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE) :

开辟了一个大小为3的容量.

2.2、断点第三次调用方法

容量为3, 占用了两个.

2.3、断点第四次调用方法

超出容量进行扩容.

扩容为8.进入reallocate(oldCapacity, newCapacity);

发现它是对一个旧的缓存进行了释放, 重新生成一个新的buckets. 这就是为什么之前测试调用所有的方法之后发现buckets里的imp的指针为空的原因了.

总结:

  • 当一个类第一次调用一次实例方法的时候, 会对这个方法进行一次方法缓存,调用 void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)方法开辟出一个容量为4的空间用来存储方法.

  • 当加载的方法缓存个数超过当前容量的四分之三的时候, 就会调用 void cache_t::expand() 方法对当前的容量进行一次扩容, 新的容量为之前容量的2倍,并且调用 void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity) 方法对旧缓存进行一次擦除, 重新开辟一个新的空间用来存放方法缓存.