iOS原理探索06--cache_t分析

概要

前面文章我們分析了isabits,本文主要分析一下cache_t和類的關系。我們知道cache是用來緩存指針和函數表的,那么底層是如何具體實現的呢?帶著問題來分析、思考一下。

cache_t的結構
  • 首先我們來看一下它的源碼實現
struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
    //模擬器或者macOS環境
    //explicit_atomic:顯示原子性,保證增刪改查的安全
    explicit_atomic<struct bucket_t *> _buckets;//存放SEL、imp
    explicit_atomic<mask_t> _mask;
    ///省略代碼....
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    //64位真機環境
    explicit_atomic<uintptr_t> _maskAndBuckets;
    mask_t _mask_unused;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    //非64位真機環境
    explicit_atomic<uintptr_t> _maskAndBuckets;
    mask_t _mask_unused;
  //省略代碼
#else
#error Unknown cache mask storage type.
#endif
    
#if __LP64__
    uint16_t _flags;
#endif
    uint16_t _occupied;

//////省略方法.......

};

上面源碼可以看出,cach_t主要包含_buckets_mask_flags_occupied四個部分,當然在不同的環境下變量名不同,以上代碼有詳細注釋,我們以MacOS環境為例。

  • 我們可以根據cach_t的源碼流程圖來探索一下每個環節的具體實現過程
    cach_t源碼實現流程圖----來自style_月月簡書
cache_t的結構解釋
  • _buckets:我們可以進入到bucket_t源碼查看一下里面的具體實現,我們可以看到無論是arm64還是其他環境下,bucket_t結構體包含了兩個東西,一個是imp、另外一個是sel。
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
}
  • _mask:指掩碼數據,用于在哈希算法或者哈希沖突算法中計算哈希下標;
  • _flags:標識
  • _occupied:表示哈希表中 sel-imp 的占用大小 (即可以理解為分配的內存中已經存儲了sel-imp的的個數);
結合示例代碼分析cache_t是否在方法調用時會被緩存
  • 示例代碼
//LGPerson.h
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *lgName;
@property (nonatomic, strong) NSString *nickName;

- (void)sayHello;

- (void)sayCode;

- (void)sayMaster;

- (void)sayNB;

+ (void)sayHappy;

@end

//LGPerson.m
@implementation LGPerson
- (void)sayHello{
    NSLog(@"LGPerson say : %s",__func__);
}

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

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

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

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

//mian.m
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        LGPerson *p  = [LGPerson alloc];
        Class pClass = [LGPerson class];
//        p.lgName     = @"Cooci";
//        p.nickName   = @"KC";
        // 緩存一次方法 sayHello
        // 4
        [p sayHello];
        [p sayCode];
        [p sayMaster];
//        [p sayNB];


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

我們先創建一個類,并添加類方法和實例方法,在main.m中初始化LGPerson并調用該方法,來探索cach_t中的各變量的值的變化。

  • 我們在[p sayHello];打個斷點,運行程序,通過lldb調試看一下cache_t的打印情況:
斷點位置 指令 輸出結果
[p sayHello] p/x pClass $0 = 0x0000000100002298 LGPerson
指針偏移16位 0x0000000100002298 + 0x10 0x00000001000022a8
... ... p (cache_t *)0x00000001000022a8 (cache_t *) $1 = 0x00000001000022a8
... ... p *$1 輸出結果見下面代碼
`p *$1`的輸出結果
(cache_t) $2 = {
  _buckets = {
    std::__1::atomic<bucket_t *> = 0x000000010032e420 {
      _sel = {
        std::__1::atomic<objc_selector *> = (null)
      }
      _imp = {
        std::__1::atomic<unsigned long> = 0
      }
    }
  }
  _mask = {
    std::__1::atomic<unsigned int> = 0
  }
  _flags = 32804
  _occupied = 0
}

根據表格內容以及輸出結果我們可以得出,在我們沒有調用方法之前,cache_t中的bucket_t,_mask,_occupied都沒有值。

  • 現在我們過一下【[p sayHello]】斷點,使用相同的方法查看一下cache_t內容
斷點位置 指令 輸出結果
... ... p (cache_t *)0x00000001000022a8 $5 = 0x00000001000022a8
... ... p *$5 輸出結果見下面代碼
(cache_t) $6 = {
  _buckets = {
    std::__1::atomic<bucket_t *> = 0x00000001007bf8c0 {
      _sel = {
        std::__1::atomic<objc_selector *> = ""
      }
      _imp = {
        std::__1::atomic<unsigned long> = 11928
      }
    }
  }
  _mask = {
    std::__1::atomic<unsigned int> = 3
  }
  _flags = 32804
  _occupied = 1
}

我們可以看出,在調用了-[LGPerson sayHello]方法后,_buckets_mask中也有值了_occupied +1,這就說明當方法被調用后就會被cache緩存起來。

  • 接下來我們證明一下上面輸出結果(cache_t) $6中的方法是不是sayHello,接著上面的步驟我們來打印一下cache_t中的_sel_imp
斷點位置 指令 輸出結果
... ... $6.buckets() $7 = 0x00000001007bf8c0
... ... p *$7 輸出結果見下面代碼
... ... p $8.sel() (SEL) $9 = "sayHello"
... ... p $8.imp(pClass) $10 = 0x0000000100000c00 (KCObjc-[LGPerson sayHello])`
(lldb) p *$7
(bucket_t) $8 = {
  _sel = {
    std::__1::atomic<objc_selector *> = ""
  }
  _imp = {
    std::__1::atomic<unsigned long> = 11928
  }
}

總結:系統在調用方法后確實會被cace_t緩存起來!那么問題來了,這些cace_t的值是怎么變化的呢?有什么作用呢?帶著這個問題我們繼續來探索一下。

探索cache的值的變化

  • 我們還是根據前面的斷點接著執行下一個sayCode方法,看一下cache中的值的變化。
斷點位置 指令 輸出結果
執行完sayCode方法 p *$5 $11輸出結果見下面代碼1
... ... p $11.buckets() $12 = 0x00000001007bf8c0
... ... p *$12 $13輸出結果見下面代碼2
... ... p $13.sel() $14 = "sayHello"
... ... 指針偏移1:p *($12 + 1) $15輸出結果見下面代碼3
... ... p $15.sel() (SEL) $16 = "sayCode"
  • $11輸出結果代碼1
(lldb) p *$5
(cache_t) $11 = {
  _buckets = {
    std::__1::atomic<bucket_t *> = 0x00000001007bf8c0 {
      _sel = {
        std::__1::atomic<objc_selector *> = ""
      }
      _imp = {
        std::__1::atomic<unsigned long> = 11928
      }
    }
  }
  _mask = {
    std::__1::atomic<unsigned int> = 3
  }
  _flags = 32804
  _occupied = 2
  • $13輸出結果2
(bucket_t) $13 = {
  _sel = {
    std::__1::atomic<objc_selector *> = ""
  }
  _imp = {
    std::__1::atomic<unsigned long> = 11928
  }
}
  • $15輸出結果3
(bucket_t) $15 = {
  _sel = {
    std::__1::atomic<objc_selector *> = ""
  }
  _imp = {
    std::__1::atomic<unsigned long> = 11944
  }
}

從上面的lldb的調試表格以及輸出結果我們可以得知一下兩點結論:第一: 無論什么時候什么方法被調用后都會被cache緩存起來;第二:隨著調用方法的數量增多,cache中的_occupied也會增加相應的數目。

注意:occupied 是如何遞增的呢?cache又是如何緩存的呢?下面小節分析一下cache_t的底層原理。

cache_t的底層原理分析

前面小節我們發現當有多個方法被調用的時候,cache_t的值就會發生改變,那么是哪個函數引起的呢?在源碼中發現了一個函數incrementOccupied,這個函數使得occupied的值進行遞增

void cache_t::incrementOccupied() 
{
    _occupied++;
}
  • 那么這個函數是在什么時候調用的呢?在源碼781中搜索一下,找到了調用這個方法的地方
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);
    }
    //當小于等于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.
    }
    else {
        //超過了3/4進行原來容量的2倍擴容
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;  // 擴容兩倍 4
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
        //重新按照擴容后的大小進行開辟空間
        reallocate(oldCapacity, capacity, true);  // 內存 庫容完畢
    }

    bucket_t *b = buckets();
    mask_t m = capacity - 1;
    //計算此次插入的開始的哈希下標
    mask_t begin = cache_hash(sel, m);
    mask_t i = begin;

    // 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.
    /*
     通過循環
     //掃描第一個未使用的插槽并插入。
     //保證有一個空槽,因為
     //最小尺寸是4,我們將大小調整為3/4滿。
     */
    do {
        //如果當前哈希下標的sel未被存儲
        if (fastpath(b[i].sel() == 0)) {
            //Occupied++
            incrementOccupied();
            //bucket對sel, imp進行set賦值
            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));
    //循環條件:當前哈希下標存儲的sel不等于即將要插入的sel,通過cache_next方法重新計算得到新的哈希下標。

    cache_t::bad_cache(receiver, (SEL)sel, cls);
}
  • cache_tinsert流程圖
    ` insert`流程圖--來自簡書style_月月
流程梳理
  • 計算當前的緩存占用數量
    mask_t newOccupied = occupied() + 1;

根據當屬性未賦值無方法調用時,此時的occupied()為0,而newOccupied為1

  • 第一次進來創建,申請開辟空間;
if (slowpath(isConstantEmptyCache())) { //小概率發生的 即當 occupied() = 0時,即創建緩存,創建屬于小概率事件
    // Cache is read-only. Replace it.
    if (!capacity) capacity = INIT_CACHE_SIZE; //初始化時,capacity = 4(1<<2 -- 100)
    reallocate(oldCapacity, capacity, /* freeOld */false); //開辟空間
    //到目前為止,if的流程的操作都是初始化創建
}
關于開辟空間的源碼解析
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld)
{
    bucket_t *oldBuckets = buckets();
    //向系統申請開辟內存,即開辟bucket,此時的bucket只是一個臨時變量
    bucket_t *newBuckets = allocateBuckets(newCapacity);

    // Cache's old contents are not propagated. 
    // This is thought to save cache memory at the cost of extra cache fills.
    // fixme re-measure this

    ASSERT(newCapacity > 0);
    ASSERT((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);

    //將newBuckets存入緩存中
    setBucketsAndMask(newBuckets, newCapacity - 1);
    
    if (freeOld) {
        //如果有舊的oldBuckets,清理之前的緩存
        cache_collect_free(oldBuckets, oldCapacity);
    }
}

第一步:向系統申請開辟內存,即開辟bucket,此時的bucket只是一個臨時變量
第二步:將newBuckets存入緩存中,如果是真機,根據bucket和mask的位置存儲,并將occupied占用設置為0,如果不是真機,正常存儲bucket和mask,并將occupied占用設置為0

//真機環境下
   _maskAndBuckets.store(((uintptr_t)newMask << maskShift) | (uintptr_t)newBuckets, std::memory_order_relaxed);
   _occupied = 0;

//模擬器環境下
   _maskAndBuckets.store(buckets | maskShift, memory_order::memory_order_relaxed);
   _occupied = 0;

第三步:如果有舊的buckets,需要清理之前的緩存,即調用cache_collect_free方法,其源碼實現如下

   if (freeOld) {
        //如果有舊的oldBuckets,清理之前的緩存
        cache_collect_free(oldBuckets, oldCapacity);
    }

//cache_collect_free方法的具體實現
    _garbage_make_room ();//創建垃圾回收空間
    garbage_byte_size += cache_t::bytesForCapacity(capacity);
    garbage_refs[garbage_count++] = data;//記錄緩存這一次的Bucket
    cache_collect(false);//垃圾回收,清理舊的Bucket緩存
  • 不是第一次創建判斷當前緩存占用數量,如果小于等于3/4不做處理,如果超過了3/4,對原來的容量進行兩倍擴容重新申請空間
   //當小于等于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.
    }
    else {
        //超過了3/4進行原來容量的2倍擴容
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;  // 擴容兩倍 4
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
        //重新按照擴容后的大小進行開辟空間
        reallocate(oldCapacity, capacity, true);  // 內存 庫容完畢
    }
  • 針對需要存儲的bucket進行內部的sel和imp賦值,首先需要計算此次的插入哈希下標,然后通過do-while循環找到合適的下標操作(判斷條件:當前哈希下標存儲的sel不等于即將要插入的sel,通過cache_next方法重新計算得到新的哈希下標。),如果當前的哈希下標為存儲sel,那么對占用數進行++,即ocuplied++;如果下標存在直接返回
//計算此次插入的開始的哈希下標
mask_t begin = cache_hash(sel, m);

//具體實現
static inline mask_t cache_hash(SEL sel, mask_t mask) 
{
    return (mask_t)(uintptr_t)sel & mask;
}


//do--while實現
 do {
        //如果當前哈希下標的sel未被存儲
        if (fastpath(b[i].sel() == 0)) {
            //Occupied++
            incrementOccupied();
            //bucket對sel, imp進行set賦值
            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));
    //循環條件:當前哈希下標存儲的sel不等于即將要插入的sel,通過cache_next方法重新計算得到新的哈希下標。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,345評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,494評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,283評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,953評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,714評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,410評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,940評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,776評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,210評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,654評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373