iOS-cache_t分析

253 阅读3分钟
struct objc_class : objc_object {
    // Class ISA;           //8
    Class superclass;       //8
    cache_t cache;          //16    // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    ......
}

1: 查看源码底层类的结构,发现第三个属性,是cache_t。

struct cache_t {        //from objc-runtime-new.h
    struct bucket_t *_buckets;  //8 byte   方法缓存的IMP&key
    mask_t _mask;  //uint32_t   //4
    mask_t _occupied;           //4  sum = 8 + 4 + 4 = 16byte
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);
    //缓存扩容,清楚old缓存,开辟新的内存,2倍扩容
    void expand();
    //重新开辟缓存的空间,一般根据算法的最优解,扩容2倍为最佳。
    void reallocate(mask_t oldCapacity, mask_t newCapacity);
    //哈希算法查找key的cache方法
    struct bucket_t * find(cache_key_t key, id receiver);
    //缓存容错处理
    static void bad_cache(id receiver, SEL sel, Class isa) __attribute__((noreturn));
}

根据struct cache_t 三个成员变量以及其相关函数方法,可以查看缓存的一些具体细节,使用lldb打印内存地址的方式可以查看验证cache_t中缓存的方法(第一次调用的方法才会进行缓存)。通过方法间的关联性可以找出缓存的关键方法cache_fill_nolock

sample——验证缓存的方法:

LGPerson *person = [[LGPerson alloc] init];
Class pClass = [LGPerson class];
[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 = {   //查找到缓存的sayHello方法。
  _key = 4294971020
  _imp = 0x0000000100000c60 (LGTest`-[LGPerson sayHello] at LGPerson.m:13)
}

验证所得,cache_t的成员变量bucket_t存储着缓存方法的最终imp地址。

2: 使用lldb打印缓存方法的验证方法比较繁琐,但也是很重要的初步的探索方法,想要更加方便的查看缓存方法,可以自定义objc_class,将原类强转为lg_objc_class,打印出缓存的imp信息。链接

3:cache_t的相关函数方法都在objc-cache.mm中,根据头部注释可查看缓存的大概流程,来分析方法缓存的策略。从注释中可找到缓存的关键方法是cache_fill_nolock

static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
    cacheUpdateLock.assertLocked();//lock断言

    //1:验证 cls initialize结束
    // Never cache before +initialize is done => ***在+initialize结束前,不会缓存。***
    if (!cls->isInitialized()) return;

    // 2:确保条目没有被其他线程添加到缓存中,在我们抓住cacheUpdateLock之前。
    // 3:若有cache 则 return ,cache_getImp:为汇编方法(大概),查找更快。
    if (cache_getImp(cls, sel)) return;
    
    //***以下为缓存具体流程

    cache_t *cache = getCache(cls);
    cache_key_t key = getKey(sel);

    //4: 验证缓存的内存空间大小是否需要扩容or 从头开辟一块空间
    // 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()) {//4.1:缓存空间为空,开辟一块空间用于缓存
        // Cache is read-only. Replace it.
        cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);//INIT_CACHE_SIZE:4
    }
    else if (newOccupied <= capacity / 4 * 3) {//4.2: < 3/4 继续缓存新的cls imp
        // Cache is less than 3/4 full. Use it as-is.
    }
    else {
        // Cache is too full. Expand it. =>扩容
        //4.3:>3/4 就扩容,扩容2倍-1,并且释放旧的缓存空间,开辟一块新的空间来缓存
        cache->expand();
    }

    //5:正式缓存
    // 哈希的方式查找一个空的slot,插入。
    bucket_t *bucket = cache->find(key, receiver);
    if (bucket->key() == 0) cache->incrementOccupied();
    bucket->set(key, imp);
}

此部分需要深入解读 cache_fill_nolock源码里的调用函数,来分析。