系統(tǒng)底層源碼分析(17)——類(lèi)結(jié)構(gòu)中的cache

上篇文章探究了類(lèi)的結(jié)構(gòu),其中提到cache,今天就來(lái)探究一下。

  • 結(jié)構(gòu)
struct objc_class : objc_object {
    // Class ISA; 
    Class superclass; // 父類(lèi)
    cache_t cache;    // 緩存          // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    ...
};
struct cache_t {
    struct bucket_t *_buckets;//緩存方法
    mask_t _mask;//緩存容量
    mask_t _occupied;//緩存?zhèn)€數(shù)
    ...
};
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
...
};
  • 作用
  1. 從結(jié)構(gòu)可以看出cache作用應(yīng)該是調(diào)用方法后對(duì)其緩存,加快之后的調(diào)用速度。我們可以寫(xiě)段代碼,檢驗(yàn)一下:
Person *person = [Person alloc];
Class pClass = [Person class];
[person sayHello];

接著斷點(diǎn)運(yùn)行打印內(nèi)存結(jié)構(gòu):

2019-12-25 00:39:22.566292+0800 Test[3586:42169] Person say : -[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 = {
  _key = 4294971020
  _imp = 0x0000000100000c60 (Test`-[Person sayHello] at Person.m:13)
}
  1. 一開(kāi)始cache沒(méi)有值,調(diào)用[person sayHello]后才有了值,由此可見(jiàn),類(lèi)的cache緩存了調(diào)用過(guò)的實(shí)例方法。從而也可以推導(dǎo)出元類(lèi)的cache緩存了調(diào)用過(guò)的類(lèi)方法:
(lldb) p/x 0x001d8001000012b9 & 0x00007ffffffffff8ULL
(unsigned long long) $5 = 0x00000001000012b8

(lldb) x/4gx 0x00000001000012b8
0x1000012b8: 0x001d800100b360f1 0x0000000100b360f0
0x1000012c8: 0x0000000101e236c0 0x0000000200000003
(lldb) p (cache_t *)0x1000012c8
(cache_t *) $6 = 0x00000001000012c8
(lldb) p *$6
(cache_t) $7 = {
  _buckets = 0x0000000101e236c0
  _mask = 3
  _occupied = 2
}
(lldb) p $7._buckets
(bucket_t *) $8 = 0x0000000101e236c0
(lldb) p *$8
(bucket_t) $9 = {
  _key = 4298994200
  _imp = 0x00000001003cc3b0 (libobjc.A.dylib`::+[NSObject alloc]() at NSObject.mm:2294)
}
  1. 我們繼續(xù)驗(yàn)證:
Person *person = [[Person alloc] init];
Class pClass = [Person class];

[person sayHello];
[person sayCode];
[person sayNB]; 

接著斷點(diǎn)運(yùn)行打印內(nèi)存結(jié)構(gòu):

(lldb) x/4gx pClass
0x1000012e8: 0x001d8001000012c1 0x0000000100b36140
0x1000012f8: 0x0000000101029950 0x0000000100000007
(lldb) p (cache_t *)0x1000012f8
(cache_t *) $1 = 0x00000001000012f8
(lldb) p *$1
(cache_t) $2 = {
  _buckets = 0x0000000101029950
  _mask = 7
  _occupied = 1
}
(lldb) p $2._buckets
(bucket_t *) $3 = 0x0000000101029950
(lldb) p *$3
(bucket_t) $4 = {
  _key = 0
  _imp = 0x0000000000000000
}
(lldb) p $2._buckets[0]
(bucket_t) $5 = {
  _key = 0
  _imp = 0x0000000000000000
}
(lldb) p $2._buckets[1]
(bucket_t) $6 = {
  _key = 0
  _imp = 0x0000000000000000
}
(lldb) p $2._buckets[2]
(bucket_t) $7 = {
  _key = 4294971026
  _imp = 0x0000000100000ce0 (Test`-[Person sayNB] at Person.m:25)
}
(lldb) p $2._buckets[3]
(bucket_t) $8 = {
  _key = 0
  _imp = 0x0000000000000000
}

測(cè)試調(diào)用多個(gè)方法時(shí),我們發(fā)現(xiàn)緩存的方法只有[Person sayNB],而且_mask3變成了7,這些看來(lái)是緩存策略影響了。

  • 源碼
  1. 我們從objc4-750源碼探究,直入主題,從cache_fill_nolock函數(shù)開(kāi)始。如果已緩存,就獲取返回:
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;//默認(rèn)0,遞增
    mask_t capacity = cache->capacity();
    if (cache->isConstantEmptyCache()) {//判斷緩存容器是否為空
        // Cache is read-only. Replace it.
        cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);//為空就創(chuàng)建
    }
    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();//超過(guò)3/4就擴(kuò)容
    }

    // 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. 第一次調(diào)用方法([person init]),還沒(méi)緩存,就調(diào)用reallocate,這時(shí)capacity為0,INIT_CACHE_SIZE為4,最終_mask會(huì)等于3
struct bucket_t *cache_t::buckets() 
{
    return _buckets; 
}

mask_t cache_t::mask() 
{
    return _mask; 
}

mask_t cache_t::occupied() 
{
    return _occupied;
}
...
mask_t cache_t::capacity() 
{
    return mask() ? mask()+1 : 0; //默認(rèn)0,有值+1
}
enum {
    INIT_CACHE_SIZE_LOG2 = 2,
    INIT_CACHE_SIZE      = (1 << INIT_CACHE_SIZE_LOG2)  //等于4
};
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
{
    bool freeOld = canBeFreed();

    bucket_t *oldBuckets = buckets();
    bucket_t *newBuckets = allocateBuckets(newCapacity);//創(chuàng)建
    ...
    setBucketsAndMask(newBuckets, newCapacity - 1);// -1 是一種算法,為了提前擴(kuò)容,更安全
    
    if (freeOld) {
        cache_collect_free(oldBuckets, oldCapacity);//清空舊的緩存, 所以擴(kuò)容緩存后,舊的緩存沒(méi)了
        cache_collect(false);
    }
}
bucket_t *allocateBuckets(mask_t newCapacity)
{
    bucket_t *newBuckets = (bucket_t *)
        calloc(cache_t::bytesForCapacity(newCapacity), 1);//初始化
    ...
    return newBuckets;
}
void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask)
{
    ...
    _buckets = newBuckets;//新的容器
    ...
    _mask = newMask;
    _occupied = 0;//歸0
}
  1. 經(jīng)過(guò)調(diào)多個(gè)方法后(最后調(diào)用[person sayNB]),_mask經(jīng)過(guò)mask()+1newCapacity - 1,此時(shí)應(yīng)為4,緩存空間超過(guò)容量的3/4(4 > 4*3/4),需要進(jìn)行擴(kuò)容:
void cache_t::expand()
{
    cacheUpdateLock.assertLocked();
    
    uint32_t oldCapacity = capacity();
    uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;//變成2倍

    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);//重置緩存大小
}

此時(shí)經(jīng)過(guò)reallocate重置緩存大小并清空舊的緩存,所以只保留了[person sayNB],而且_mask變成2倍為8,再通過(guò)newCapacity - 1變成7

  1. 緩存容量調(diào)整好后,方法最終都會(huì)通過(guò)bucket->set(key, imp)緩存下來(lái),方法數(shù)量也會(huì)通過(guò)incrementOccupied記錄:
void cache_t::incrementOccupied() 
{
    _occupied++;
}
void bucket_t::set(cache_key_t newKey, IMP newImp)
{
    assert(_key == 0  ||  _key == newKey);
    
    _imp = newImp;
    
    if (_key != newKey) {
        mega_barrier();
        _key = newKey;
    }
}
  • 總結(jié)
  • 方法緩存是為了提高程序的執(zhí)行效率;
  • 類(lèi)的cache用來(lái)緩存實(shí)例方法;
  • 元類(lèi)的cache用來(lái)緩存類(lèi)方法;
  • 如果已有緩存就獲取返回;如果沒(méi)有緩存就會(huì)創(chuàng)建容器緩存;如果緩存超出容量的3/4就會(huì)擴(kuò)容,變成2倍,并且清空舊的緩存;
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容