oc-底層原理分析之Cache_t

類的結(jié)構(gòu)分析一文中我們探索了類的底層定義,其中的屬性Cache_t我們并沒有深入研究蹭睡,這一篇文章我們來深入探索一下Cache_t

注意:以下的源碼解讀都是在mac電腦上運行衍菱,也就是說基于x86的結(jié)構(gòu),請記住這一點

什么是Cache_t

要搞清楚什么是Cache_tCache_t用來做什么肩豁,我們先看看在objc源碼中脊串,Cache_t的定義

struct cache_t {
    explicit_atomic<struct bucket_t *> _buckets;
    explicit_atomic<mask_t> _mask;
    uint16_t _occupied;

public:
    static bucket_t *emptyBuckets();
    
    struct bucket_t *buckets();
    mask_t mask();
    mask_t occupied();
    void incrementOccupied();
    void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);
    void initializeToEmpty();
    //部分代碼已略
}

通過源碼我們看到Cache_t結(jié)構(gòu)體中定義了三個屬性:

  1. _buckets
  2. _mask
  3. _occupied

但是我們現(xiàn)在并不知道這三個屬性用來做什么,要搞清楚這三個屬性的作用清钥,我們通過一個例子來探索一下

先定義一個WPerson類:

@interface WPerson : NSObject

@property (nonatomic, copy) NSString *lgName;
@property (nonatomic, strong) NSString *nickName;

- (void)sayHello;
- (void)sayCode;
- (void)sayMaster;
- (void)sayNB;
+ (void)sayHappy;
@end
@implementation WPerson
- (void)sayHello{
    NSLog(@"WPerson say : %s",__func__);
}

- (void)sayCode{
    NSLog(@"WPerson say : %s",__func__);
}

- (void)sayMaster{
    NSLog(@"WPerson say : %s",__func__);
}

- (void)sayNB{
    NSLog(@"WPerson say : %s",__func__);
}

+ (void)sayHappy{
    NSLog(@"WPerson say : %s",__func__);
}
@end

現(xiàn)在我們創(chuàng)建一個WPerson對象琼锋,然后調(diào)用sayHello方法:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        WPerson *p  = [WPerson alloc];
        Class pClass = [WPerson class];
        [p sayHello];

        NSLog(@"%@",pClass);
    }
    return 0;
}

Cache_t 結(jié)構(gòu)探索

先找到pClass的首地址:

  • x/4gx pClass:以16進制形式打印出pClass地址

    0x100002288: 0x0000000100002260 0x0000000100334140
    0x100002298: 0x00000001006f4050 0x0001802400000003
    

    pClass首地址為:0x100002288

  • 通過在類結(jié)構(gòu)分析一文中我們得知了類的結(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
    
        class_rw_t *data() const {
            return bits.data();
        }
    }
    

    由于isasuperclass都占用8個字節(jié),所以我們要訪問到cache祟昭,我們需要將首地址偏移16字節(jié)缕坎,所以:

    (lldb) p (cache_t *)0x100002298
    (cache_t *) $1 = 0x0000000100002298
    

    我們得到了cache的地址

  • 訪問cache.buckets(),我們知道_buckets是一個數(shù)組,所以我們先訪問第一個值看存儲的是什么

    (lldb) p $2.buckets()[0]
    (bucket_t) $3 = {
    _sel = {
    std::__1::atomic<objc_selector *> = ""
    }
    _imp = {
    std::__1::atomic<unsigned long> = 11912
    }
    

}
```
我們得到一個bucket_t結(jié)構(gòu)篡悟,我們再看看bucket_t的源碼:

```c
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__
    explicit_atomic<uintptr_t> _imp;
    explicit_atomic<SEL> _sel;
#else
    explicit_atomic<SEL> _sel;
    explicit_atomic<uintptr_t> _imp;
#endif

public:
    inline SEL sel() const { return _sel.load(memory_order::memory_order_relaxed); }

    inline IMP imp(Class cls) const {
        uintptr_t imp = _imp.load(memory_order::memory_order_relaxed);
        if (!imp) return nil;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
        SEL sel = _sel.load(memory_order::memory_order_relaxed);
        return (IMP)
            ptrauth_auth_and_resign((const void *)imp,
                                    ptrauth_key_process_dependent_code,
                                    modifierForSEL(sel, cls),
                                    ptrauth_key_function_pointer, 0);
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
        return (IMP)(imp ^ (uintptr_t)cls);
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
        return (IMP)imp;
#else
#error Unknown method cache IMP encoding.
#endif
    }

//部分代碼已略去
};
```
我們看到`bucket_t`有兩個屬性`_sel`和`_imp`谜叹,看到這里是不是很熟悉,但是別急搬葬,我們先來打印一下sel的值
  • 打印sel

    (lldb) p $3.sel()
    (SEL) $4 = "sayHello"
    

我們看到結(jié)果打印出了我們剛剛調(diào)用的方法sayHello荷腊,我們?nèi)绻嗾{(diào)用幾個方法,這里可以打印出多個方法
所以我們得出結(jié)論:

cache_t用來緩存類的sel以及imp

既然我們知道了cache_t用來緩存類的方法踩萎,那么還有一些疑問:

  1. 緩存的策略是什么呢停局?
  2. 如果空間不足,如何對空間進行擴容香府?
  3. 緩存又是怎么讀取的董栽?(這部分內(nèi)容接下來會補上)

帶著這三個疑問,我們開始探索

cache_t緩存策略

我們先來看看insert()方法

ALWAYS_INLINE
void cache_t::insert(Class cls, SEL sel, IMP imp, id receiver)
{
#if CONFIG_USE_CACHE_LOCK
    cacheUpdateLock.assertLocked();
#else
    runtimeLock.assertLocked();
#endif

    ASSERT(sel != 0 && cls->isInitialized());

    // Use the cache as-is if it is less than 3/4 full
    mask_t newOccupied = occupied() + 1;
    unsigned oldCapacity = capacity(), capacity = oldCapacity;
    if (slowpath(isConstantEmptyCache())) {
        // Cache is read-only. Replace it.
        if (!capacity) capacity = INIT_CACHE_SIZE;
        reallocate(oldCapacity, capacity, /* freeOld */false);
    }
    else if (fastpath(newOccupied + CACHE_END_MARKER <= capacity / 4 * 3)) { // 4  3 + 1 bucket cache_t
        // Cache is less than 3/4 full. Use it as-is.
    }
    else {
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;  // 擴容兩倍 4
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
        reallocate(oldCapacity, capacity, true);  // 內(nèi)存 庫容完畢
}

從這里我們可以看到:

  1. 如果buckets還未初始化企孩,則會先調(diào)用reallocate()方法對buckets進行初始化锭碳,初始的存儲大小為INIT_CACHE_SIZE我們看到INIT_CACHE_SIZE定義為(1 << INIT_CACHE_SIZE_LOG2)也就是4
  2. 如果本次插入后所占用的空間小于總空間的3/4時,則直接進行數(shù)據(jù)插入
  3. 如果本次插入后所占用的空間>=3/4勿璃,則需要對總空間進行擴容擒抛,如何進行的擴容,在cache_t擴容部分會有講解

我們知道了在_buckets中存儲的是bucket_t類型补疑,當數(shù)據(jù)insert的時候歧沪,都會創(chuàng)建一個bucket_t變量

mask

_buckets是一個數(shù)組,如果我們要通過某個方法的sel去查找imp莲组,我們怎么查找呢诊胞?我們大概率會想去去遍歷_buckets,但是這樣的效率是低下的锹杈,每一次的方法查找都會遍歷整個緩存撵孤,那么有沒有什么辦法能不遍歷呢迈着?

我們來看看源碼中采用的方式,我們在源碼中能看到這樣一個方法:

static inline mask_t cache_hash(SEL sel, mask_t mask) 
{
    return (mask_t)(uintptr_t)sel & mask;
}

mask傳入的是mask_t m = capacity - 1;也就是當前的容量 - 1邪码。通過和mask相與裕菠,我們得到的數(shù)字肯定是小于等于mask的,通過這種方式就可以得到sel和數(shù)組index的對應(yīng)關(guān)系闭专,在查找的時候就可以直接通過sel得到數(shù)組對應(yīng)的index奴潘,不再需要遍歷整個數(shù)組

但是你可能有一個疑問,這樣不會出現(xiàn)編碼的沖突嗎喻圃?不同的sel會不會得到同一個index呢萤彩?答案是會的,源碼中也解決了這個問題

bucket_t *b = buckets();
mask_t m = capacity - 1;
mask_t begin = cache_hash(sel, m);
mask_t i = begin;
    
do {
    if (fastpath(b[i].sel() == 0)) {
        incrementOccupied();
        b[i].set<Atomic, Encoded>(sel, imp, cls);
        return;
    }
    if (b[i].sel() == sel) {
        // The entry was added to the cache by some other thread
        // before we grabbed the cacheUpdateLock.
        return;
    }
} while (fastpath((i = cache_next(i, m)) != begin));

如果index存在了斧拍,就會調(diào)用cache_next重新生成一個index來存儲雀扶,直到找到合適的位置

cache_t擴容

capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;  // 擴容兩倍
if (capacity > MAX_CACHE_SIZE) {
    capacity = MAX_CACHE_SIZE;
}
reallocate(oldCapacity, capacity, true);  // 內(nèi)存 庫容完畢

我們可以看到擴容的原則是當前容量的兩倍,并且擴容時肆汹,重新調(diào)用reallocate將原來的數(shù)據(jù)清空愚墓。也就是說擴容后,原來的數(shù)據(jù)將不存在昂勉,重新調(diào)用原有方法的時候才會重新進行緩存浪册,如果你這時候去打印cache中的所有數(shù)據(jù),得到的并不是你當前調(diào)用的所有方法岗照,也能得到驗證

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末村象,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子攒至,更是在濱河造成了極大的恐慌厚者,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,843評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件迫吐,死亡現(xiàn)場離奇詭異库菲,居然都是意外死亡,警方通過查閱死者的電腦和手機志膀,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,538評論 3 392
  • 文/潘曉璐 我一進店門熙宇,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人溉浙,你說我怎么就攤上這事烫止。” “怎么了戳稽?”我有些...
    開封第一講書人閱讀 163,187評論 0 353
  • 文/不壞的土叔 我叫張陵馆蠕,是天一觀的道長。 經(jīng)常有香客問我,道長荆几,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,264評論 1 292
  • 正文 為了忘掉前任赊时,我火速辦了婚禮吨铸,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘祖秒。我一直安慰自己诞吱,他們只是感情好,可當我...
    茶點故事閱讀 67,289評論 6 390
  • 文/花漫 我一把揭開白布竭缝。 她就那樣靜靜地躺著房维,像睡著了一般。 火紅的嫁衣襯著肌膚如雪抬纸。 梳的紋絲不亂的頭發(fā)上咙俩,一...
    開封第一講書人閱讀 51,231評論 1 299
  • 那天,我揣著相機與錄音湿故,去河邊找鬼阿趁。 笑死,一個胖子當著我的面吹牛坛猪,可吹牛的內(nèi)容都是我干的脖阵。 我是一名探鬼主播,決...
    沈念sama閱讀 40,116評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼墅茉,長吁一口氣:“原來是場噩夢啊……” “哼命黔!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起就斤,我...
    開封第一講書人閱讀 38,945評論 0 275
  • 序言:老撾萬榮一對情侶失蹤悍募,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后战转,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體搜立,經(jīng)...
    沈念sama閱讀 45,367評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,581評論 2 333
  • 正文 我和宋清朗相戀三年槐秧,在試婚紗的時候發(fā)現(xiàn)自己被綠了啄踊。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,754評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡刁标,死狀恐怖颠通,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情膀懈,我是刑警寧澤顿锰,帶...
    沈念sama閱讀 35,458評論 5 344
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響硼控,放射性物質(zhì)發(fā)生泄漏刘陶。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,068評論 3 327
  • 文/蒙蒙 一牢撼、第九天 我趴在偏房一處隱蔽的房頂上張望匙隔。 院中可真熱鬧,春花似錦熏版、人聲如沸纷责。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,692評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽再膳。三九已至,卻和暖如春曲横,著一層夾襖步出監(jiān)牢的瞬間喂柒,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,842評論 1 269
  • 我被黑心中介騙來泰國打工胜榔, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留胳喷,地道東北人。 一個月前我還...
    沈念sama閱讀 47,797評論 2 369
  • 正文 我出身青樓夭织,卻偏偏與公主長得像吭露,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子尊惰,可洞房花燭夜當晚...
    茶點故事閱讀 44,654評論 2 354