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

上篇文章探究了類的結(jié)構(gòu)纵诞,其中提到cache逼裆,今天就來探究一下。

  • 結(jié)構(gòu)
struct objc_class : objc_object {
    // Class ISA; 
    Class superclass; // 父類
    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作用應該是調(diào)用方法后對其緩存,加快之后的調(diào)用速度。我們可以寫段代碼尚猿,檢驗一下:
Person *person = [Person alloc];
Class pClass = [Person class];
[person sayHello];

接著斷點運行打印內(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. 一開始cache沒有值,調(diào)用[person sayHello]后才有了值楣富,由此可見凿掂,類的cache緩存了調(diào)用過的實例方法。從而也可以推導出元類的cache緩存了調(diào)用過的類方法:
(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ù)驗證:
Person *person = [[Person alloc] init];
Class pClass = [Person class];

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

接著斷點運行打印內(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
}

測試調(diào)用多個方法時菩彬,我們發(fā)現(xiàn)緩存的方法只有[Person sayNB]缠劝,而且_mask3變成了7潮梯,這些看來是緩存策略影響了骗灶。

  • 源碼
  1. 我們從objc4-750源碼探究,直入主題秉馏,從cache_fill_nolock函數(shù)開始耙旦。如果已緩存,就獲取返回:
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;//默認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();//超過3/4就擴容
    }

    // 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]),還沒緩存帆竹,就調(diào)用reallocate绕娘,這時capacity為0,INIT_CACHE_SIZE為4栽连,最終_mask會等于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; //默認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 是一種算法,為了提前擴容秒紧,更安全
    
    if (freeOld) {
        cache_collect_free(oldBuckets, oldCapacity);//清空舊的緩存, 所以擴容緩存后绢陌,舊的緩存沒了
        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)過調(diào)多個方法后(最后調(diào)用[person sayNB]),_mask經(jīng)過mask()+1newCapacity - 1熔恢,此時應為4脐湾,緩存空間超過容量的3/4(4 > 4*3/4),需要進行擴容:
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);//重置緩存大小
}

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

  1. 緩存容量調(diào)整好后闻鉴,方法最終都會通過bucket->set(key, imp)緩存下來,方法數(shù)量也會通過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í)行效率衅谷;
  • 類的cache用來緩存實例方法椒拗;
  • 元類的cache用來緩存類方法;
  • 如果已有緩存就獲取返回;如果沒有緩存就會創(chuàng)建容器緩存蚀苛;如果緩存超出容量的3/4就會擴容在验,變成2倍,并且清空舊的緩存堵未;
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末腋舌,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子渗蟹,更是在濱河造成了極大的恐慌块饺,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件雌芽,死亡現(xiàn)場離奇詭異授艰,居然都是意外死亡,警方通過查閱死者的電腦和手機世落,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門淮腾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人屉佳,你說我怎么就攤上這事谷朝。” “怎么了武花?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵圆凰,是天一觀的道長。 經(jīng)常有香客問我体箕,道長专钉,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任干旁,我火速辦了婚禮驶沼,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘争群。我一直安慰自己回怜,他們只是感情好,可當我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布换薄。 她就那樣靜靜地躺著玉雾,像睡著了一般。 火紅的嫁衣襯著肌膚如雪轻要。 梳的紋絲不亂的頭發(fā)上复旬,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天,我揣著相機與錄音冲泥,去河邊找鬼驹碍。 笑死壁涎,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的志秃。 我是一名探鬼主播怔球,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼浮还!你這毒婦竟也來了竟坛?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤钧舌,失蹤者是張志新(化名)和其女友劉穎担汤,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體洼冻,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡崭歧,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了碘赖。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片驾荣。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖普泡,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情审编,我是刑警寧澤撼班,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站垒酬,受9級特大地震影響砰嘁,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜勘究,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一矮湘、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧口糕,春花似錦缅阳、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至超棺,卻和暖如春向族,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背棠绘。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工件相, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留再扭,地道東北人。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓夜矗,卻偏偏與公主長得像霍衫,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子侯养,可洞房花燭夜當晚...
    茶點故事閱讀 42,762評論 2 345