cache原理分析

前言:

在類的結(jié)構(gòu)分析這篇分析了objc_class 結(jié)構(gòu)體內(nèi)部的isa和bit屬性,那麼這次就分析其中的cache屬性。

cache分析

部分源碼:

struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
    //macOS鳖宾、模擬器
    //explicit_atomic 原子性瑞信,目的是為了能夠保證增刪改查線程的安全性光羞。
    //等價於struct bucket_t * _buckets;
    //_buckets 中放的是 sel imp
    //_buckets的讀取有提供相應(yīng)名稱的方法buckets()
    explicit_atomic<struct bucket_t *> _buckets;
    explicit_atomic<mask_t> _mask;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16 //64位真機(jī)
    explicit_atomic<uintptr_t> _maskAndBuckets;//寫在一起的目的是為了優(yōu)化
    mask_t _mask_unused;
    //以下都是掩碼绩鸣,即面具 -- 類似于isa的掩碼,即位域
    // 掩碼省略....

#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4 //非64位 真機(jī)
    explicit_atomic<uintptr_t> _maskAndBuckets;
    mask_t _mask_unused;

    //以下都是掩碼纱兑,即面具 -- 類似于isa的掩碼呀闻,即位域
    // 掩碼省略....
#else
#error Unknown cache mask storage type.
#endif

#if __LP64__
    uint16_t _flags;
#endif
    uint16_t _occupied;
    //方法省略.....
}

  • 以上是cache_t 源碼,可以看到三個架構(gòu)處理潜慎,其中真機(jī)架構(gòu)中的mask和bucket是寫在一起总珠,目的是為了優(yōu)化,透過不同的架構(gòu)提供的掩碼來獲取相應(yīng)的數(shù)據(jù)勘纯。

    • CACHE_MASK_STORAGE_OUTLINED:運(yùn)行在macOS或是模擬器
    • CACHE_MASK_STORAGE_HIGH_16 :運(yùn)行在64位的真機(jī)
    • CACHE_MASK_STORAGE_LOW_4 :運(yùn)行在非64位的真機(jī)
  • 可以看到cache_t 源碼內(nèi)部有bucket_t 結(jié)構(gòu)體,內(nèi)部存放selimp

    struct bucket_t {
    private:
    #if __arm64__ //真機(jī)
        //explicit_atomic 是加了原子性的保護(hù)
        explicit_atomic<uintptr_t> _imp;
        explicit_atomic<SEL> _sel;
    #else //非真機(jī)
        explicit_atomic<SEL> _sel;
        explicit_atomic<uintptr_t> _imp;
    #endif
        //方法等其他部分省略
    }
    
    

查找cache中的sel-imp

cache_t 中查找存儲的sel-imp,有以下兩種方式

  • 通過源碼
  • 脫離源碼環(huán)境環(huán)境調(diào)適分析

事前準(zhǔn)備

  • 定義一個LGPeron類的聲明
  • LGPeron類的實(shí)現(xiàn)
  • 在main中定義LGPerson類型指針p钓瞭,並調(diào)用其中其中的三個實(shí)例方法驳遵,在p調(diào)用第一個方法第一個方法處加一個斷點(diǎn)。
  • ldb查找流程如下
  • 透過獲取pClass的首地址山涡,偏移16字節(jié)後堤结,得到了cache地址
  • 從源碼分析中唆迁,我們知道sel-impcache_t_buckets屬性 中,而在cache_t結(jié)構(gòu)體中提供了獲取_buckets屬性的方法buckets()
  • 獲取了_buckets屬性竞穷,就可以獲取sel-imp了唐责,這兩個的獲取在bucket_t結(jié)構(gòu)體中同樣提供了相應(yīng)的獲取方法sel()以及imp(pClass)
  • 可以從上面的流程看到,在未執(zhí)行方法前瘾带,緩存是沒有沒有緩存方法的鼠哥,而在調(diào)用方法後,緩存內(nèi)就有一個緩存看政。
  • 我們現(xiàn)在了解如何獲取sel-imp,我們可以透過machOView來進(jìn)行確認(rèn)朴恳,將二進(jìn)制文件拖入machOView 可以在Function段內(nèi)查看到地址與我們lldb打印出來的地址一樣
  • 透過上面的成功獲取了sel-imp,那麼我們接著再打一個斷點(diǎn)在sayCode上允蚣,過掉斷點(diǎn)後于颖,執(zhí)行了sayCode方法,用上面的步驟嚷兔,先打印cache森渐,可以看到_occupied = 2 ,接著打印buckets以及透過buckets的首地址偏移拿到第二個sel-imp冒晰,然後打印第二個sel 及 imp同衣,即可看到結(jié)果。

脫離源碼調(diào)適分析

  • 上述可透過源碼的環(huán)境進(jìn)行l(wèi)ldb動態(tài)調(diào)適翩剪,那麼我們來試試如果不使用源碼環(huán)境的狀況下乳怎,要如何分析sel-imp呢?
  • 其實(shí)也就是將所需部分的源碼前弯,拷貝至文件中蚪缀,完整程式碼如下
typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits

struct lg_bucket_t {
    SEL _sel;
    IMP _imp;
};

struct lg_cache_t {
    struct lg_bucket_t * _buckets;
    mask_t _mask;
    uint16_t _flags;
    uint16_t _occupied;
};

struct lg_class_data_bits_t {
    uintptr_t bits;
};

struct lg_objc_class {
    Class ISA;
    Class superclass;
    struct lg_cache_t cache;             // formerly cache pointer and vtable
    struct lg_class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *p  = [LGPerson alloc];
        Class pClass = [LGPerson class];  // objc_clas
        [p say1];
        [p say2];
        //[p say3];
        //[p say4];

        struct lg_objc_class *lg_pClass = (__bridge struct lg_objc_class *)(pClass);
        NSLog(@"%hu - %u",lg_pClass->cache._occupied,lg_pClass->cache._mask);
        for (mask_t i = 0; i<lg_pClass->cache._mask; i++) {
            // 打印獲取的 bucket
            struct lg_bucket_t bucket = lg_pClass->cache._buckets[i];
            NSLog(@"%@ - %p",NSStringFromSelector(bucket._sel),bucket._imp);
        }

        NSLog(@"Hello, World!");
    }
    return 0;
}

  • 由於objc_classISA屬性是繼承自objc_object的,我們將源碼拷貝到自己的文件時恕出,就缺少了繼承關(guān)係询枚,所以需要聲明這個成員變量,即添加 Class ISAstruct lg_objc_class中浙巫,否則會有下圖之狀況金蜀。
  • 在lg_objc_class增加Class ISA屬性。
  • 增加兩個方法say3 say4的畴。

我們可以從上面的打印結(jié)果渊抄,發(fā)現(xiàn)以下的問題?

  • 什麼是_mask丧裁, _occupied
  • 為什麼通過方法調(diào)用的增加护桦,打印的_occupied_mask 會變化
  • bucket 數(shù)據(jù)為什麼丟失情況會有?
  • 為什麼是say4先打印,say3後打印煎娇,明顯順序有問題?為什麼是say4先打印二庵,say3後打印贪染,明顯順序有問題?
  • 打印的cache_t中的_ocupied 為什麼是從2開始?

Cache_t底層原理分析

  • 透過源碼struct cache_t找到incrementOccupied()函數(shù),查看incrementOccupied()具體實(shí)現(xiàn)
  • incrementOccupied()具體實(shí)現(xiàn)如下
  • 我們可以在全局環(huán)境中搜索incrementOccupied()催享,看看有哪些地方調(diào)用了incrementOccupied()杭隙,可以看到只有在cache_t的Insert有調(diào)用。
  • cache_t::insert方法為cache_t 的插入因妙,cache_t 內(nèi)部儲存的是sel-imp 痰憎,以下為從insert方法調(diào)用分析,如下為流程圖

insert方法分析

  • 以下為詳細(xì)註釋

主要分為三個流程區(qū)塊

  • 流程一:計算當(dāng)前的緩存佔(zhàn)用量
  • 流程二:根據(jù)計算後的緩存佔(zhàn)用量進(jìn)行不同的流程操作
  • 流程三:對需要儲存的bucket進(jìn)行進(jìn)行內(nèi)部imp和sel賦值

流程一

mask_t newOccupied = occupied() + 1;

  • 當(dāng)沒有任何屬性被賦值以及無方法調(diào)用時兰迫,此時occupied為0信殊,而newOccupied就變?yōu)?。
  • 當(dāng)有屬性被賦值時汁果,會調(diào)用set方法涡拘,occupied會增加,當(dāng)有多個屬性賦值時据德,就會依照此邏輯增加鳄乏。
  • 當(dāng)有方法調(diào)用時,occupied也會增加棘利,當(dāng)有多個方法調(diào)用時橱野,就會依照此邏輯增加。

流程二

  • 第一次創(chuàng)建善玫,默認(rèn)開闢4個(1左移兩位)水援。
if (slowpath(isConstantEmptyCache())) {
        //slowpath發(fā)生機(jī)率較小,即當(dāng)occupied()=0時茅郎,及創(chuàng)建緩存蜗元,創(chuàng)建屬於發(fā)生機(jī)率較小的事件。
        // Cache is read-only. Replace it.
        if (!capacity) capacity = INIT_CACHE_SIZE;
        //初始化時系冗,capacity = 4(1左移兩位)
        reallocate(oldCapacity, capacity, /* freeOld */false);
        //開闢空間
        //if 的流程為初始化創(chuàng)建
    }

  • 如果緩存佔(zhàn)用量小於等於3/4奕扣,則不做任何處理。
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.
        // 如果<=佔(zhàn)用內(nèi)存的3/4,不做任何事情 確保沒有超過佔(zhàn)用內(nèi)存的3/4
    }

  • 如果緩存佔(zhàn)用量超過3/4,則需要進(jìn)行兩倍擴(kuò)容以及重新開闢空間
else {
        //如果超出3/4 則需要擴(kuò)容(兩倍擴(kuò)容) 例如occupied為2時掌敬,剛好等於惯豆,不需擴(kuò)容
        //擴(kuò)容算法:有capacity,擴(kuò)容兩倍奔害,沒有capacity進(jìn)行初始化為4
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;  // 擴(kuò)容兩倍 4
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
        //滿了楷兽,重新梳理
        reallocate(oldCapacity, capacity, true);// 內(nèi)存 擴(kuò)容完畢
    }

reallocate方法:開闢空間

  • 第一次創(chuàng)建以及兩倍擴(kuò)容時都會使用,源碼如圖所示华临。
  • allocateBuckets 方法:向系統(tǒng)開闢bucket,此時的bucket只是一個臨時變量

  • setBucketsAndMask 方法:將臨時的bucket存入緩存中芯杀,此時的儲存分為為兩種情況:

    • 如果是真機(jī),根據(jù)bucketmask的位置儲存,並將occupied佔(zhàn)用設(shè)置為0

    • 如果不是真機(jī)瘪匿,正常儲存bucketmask 並將occupied佔(zhàn)用設(shè)置為0

    • 如果有舊的buckets,需要清理之前的緩存寻馏,即調(diào)用cache_collect_free方法棋弥,其源碼實(shí)現(xiàn)如下

    cache_collect_free方法實(shí)現(xiàn)如下:

    • _garbage_make_room :創(chuàng)建垃圾回收桶

      • 如果是第一次,需要分配回收空間
      • 如果不是第一次 诚欠,則將內(nèi)存段加大顽染,即原有的兩倍
      • 紀(jì)錄存儲這次的bucket
    • cache_collect 方法:垃圾回收,清理舊的bucket

    流程三

    主要根據(jù)cache_hash ****方法轰绵,哈希算法粉寞,計算sel-imp 存儲的下標(biāo),有以下幾種狀況

    • 如果哈希下標(biāo)的位置未存儲sel 左腔,表示該下標(biāo)位置獲取sel等於0,將此時sel-imp存儲進(jìn)去唧垦,並將occupied佔(zhàn)用加1
    • 如果當(dāng)前哈希下標(biāo)存儲sel等於即將插入的sel則直接返回。
    • 如果當(dāng)前哈希下標(biāo)存儲的sel不等於即將插入的sel液样,則重新通過cache_next方法即哈希衝突算法振亮,重新進(jìn)行哈希計算,得到新的下標(biāo)鞭莽,再去對比進(jìn)行存儲坊秸。

    其中的哈希算法哈希衝突算法

    • cache_hash:哈希算法
    static inline mask_t cache_hash(SEL sel, mask_t mask) 
    {
        return (mask_t)(uintptr_t)sel & mask; // 通過sel & mask(mask = cap -1)
    }
    
    
    • cache_next:哈希衝突算法
    #if __arm__  ||  __x86_64__  ||  __i386__
    // objc_msgSend has few registers available.
    // Cache scan increments and wraps at special end-marking bucket.
    #define CACHE_END_MARKER 1
    static inline mask_t cache_next(mask_t i, mask_t mask) {
        return (i+1) & mask; //(將當(dāng)前的哈希下標(biāo) +1) & mask,重新進(jìn)行哈希計算澎怒,得到一個新的下標(biāo)
    }#elif __arm64__
    // objc_msgSend has lots of registers available.
    // Cache scan decrements. No end marker needed.
    #define CACHE_END_MARKER 0
    static inline mask_t cache_next(mask_t i, mask_t mask) {
        return i ? i-1 : mask; //如果i是空褒搔,則為mask,mask = cap -1喷面,如果不為空星瘾,則 i-1,向前插入sel-imp
    }
    
    
    • 以上為cache_t的原理分析

    答疑

    • _mask是什麼乖酬?

    _mask是指掩碼數(shù)據(jù)死相,用於在哈希算法另一哈希沖突算法中計算哈希下標(biāo),其中 mask = capacity - 1

    • _occupied是什麼咬像?

    表示(hash-table)哈希表中sel-imp佔(zhàn)用大小 (分配內(nèi)存中已經(jīng)存儲了sel-imp的個數(shù))

    • init會導(dǎo)致occupied變化

    • 屬性賦值算撮,也會隱式調(diào)用,導(dǎo)致occupied變化

    • 方法調(diào)用县昂,導(dǎo)致_occupied變化

    • 為什麼通過方法調(diào)用的增加肮柜,其打印的_occupiedmask會變化

    因為在cache初始化時倒彰,分配的空間是4個审洞,通過方法調(diào)用的增量,當(dāng)存儲的sel-imp個數(shù),即newOccupied + CACHE_END_MARKER(等於1)的和 超過 總?cè)萘康?/4芒澜,例如當(dāng)occupied等於2時仰剿,newOccupied就等於3上述總和為4痴晦,就需要對cache的內(nèi)存進(jìn)行兩倍擴(kuò)容 南吮。

    • bucket數(shù)據(jù)為什麼丟失的情況會有?誊酌,例如2-7中部凑,只有say3,say4方法有函數(shù)指針

    原因是在擴(kuò)容時碧浊,是將原有的內(nèi)存全部清除了涂邀,再重新申請內(nèi)存導(dǎo)致的。

    • 為什麼是say4先打印箱锐,say3後打印比勉,明顯順序有問題?

    因為sel-imp的存儲是通過哈希算法計算下標(biāo)的,其計算的下標(biāo)有可能已經(jīng)存儲了sel瑞躺,所以又需要通過哈希衝突重新計算哈希下標(biāo)敷搪,所以導(dǎo)致下標(biāo)是隨機(jī)的,並不是固定的幢哨。

    • 打印的cache_t中的_ocupied為什麼是從2開始赡勘?

    這裡是因為LGPerson通過alloc創(chuàng)建的對象,並導(dǎo)致兩個屬性賦值的原因捞镰,屬性賦值闸与,會隱式調(diào)用set方法,set方法的調(diào)用也會導(dǎo)致occupied變化岸售。

流程圖

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末践樱,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子凸丸,更是在濱河造成了極大的恐慌拷邢,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,122評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件屎慢,死亡現(xiàn)場離奇詭異瞭稼,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)腻惠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評論 3 395
  • 文/潘曉璐 我一進(jìn)店門环肘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人集灌,你說我怎么就攤上這事悔雹。” “怎么了?”我有些...
    開封第一講書人閱讀 164,491評論 0 354
  • 文/不壞的土叔 我叫張陵腌零,是天一觀的道長梯找。 經(jīng)常有香客問我,道長益涧,這世上最難降的妖魔是什么初肉? 我笑而不...
    開封第一講書人閱讀 58,636評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮饰躲,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘臼隔。我一直安慰自己嘹裂,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評論 6 392
  • 文/花漫 我一把揭開白布摔握。 她就那樣靜靜地躺著寄狼,像睡著了一般。 火紅的嫁衣襯著肌膚如雪氨淌。 梳的紋絲不亂的頭發(fā)上泊愧,一...
    開封第一講書人閱讀 51,541評論 1 305
  • 那天,我揣著相機(jī)與錄音盛正,去河邊找鬼删咱。 笑死,一個胖子當(dāng)著我的面吹牛豪筝,可吹牛的內(nèi)容都是我干的痰滋。 我是一名探鬼主播,決...
    沈念sama閱讀 40,292評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼续崖,長吁一口氣:“原來是場噩夢啊……” “哼敲街!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起严望,我...
    開封第一講書人閱讀 39,211評論 0 276
  • 序言:老撾萬榮一對情侶失蹤多艇,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后像吻,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體峻黍,經(jīng)...
    沈念sama閱讀 45,655評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評論 3 336
  • 正文 我和宋清朗相戀三年萧豆,在試婚紗的時候發(fā)現(xiàn)自己被綠了奸披。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,965評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡涮雷,死狀恐怖阵面,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤样刷,帶...
    沈念sama閱讀 35,684評論 5 347
  • 正文 年R本政府宣布仑扑,位于F島的核電站,受9級特大地震影響置鼻,放射性物質(zhì)發(fā)生泄漏镇饮。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評論 3 329
  • 文/蒙蒙 一箕母、第九天 我趴在偏房一處隱蔽的房頂上張望储藐。 院中可真熱鬧,春花似錦嘶是、人聲如沸钙勃。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽辖源。三九已至,卻和暖如春希太,著一層夾襖步出監(jiān)牢的瞬間克饶,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評論 1 269
  • 我被黑心中介騙來泰國打工誊辉, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留矾湃,地道東北人。 一個月前我還...
    沈念sama閱讀 48,126評論 3 370
  • 正文 我出身青樓堕澄,卻偏偏與公主長得像洲尊,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子奈偏,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評論 2 355