分析objc_class中的cache

指針偏移&讀取bits信息& class_rw_t文章中我們已經(jīng)分析了bits今天我們分析cache 看看 cache是如何工作的
首先準(zhǔn)備在源碼環(huán)境下創(chuàng)建如下代碼并斷言

 @interface LGPerson : NSObject
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, strong) NSString *name;

-(void)lookBeauty;
-(void)sayNB;
-(void)listenStory;
@end

#import "LGPerson.h"

@implementation LGPerson
-(void)lookBeauty
{
    NSLog(@"看美女");
}
-(void)sayNB
{
    NSLog(@"吹牛皮");
}
-(void)listenStory
{
    NSLog(@"聽故事");
}
@end

lldb 調(diào)試獲取cache

斷點(diǎn)在lookBeauty位置


截屏2020-09-18 下午4.56.12.png

lldb調(diào)試cache 2.jpg

斷點(diǎn)過 lookBeauty 斷點(diǎn)到 sayNB


lldb調(diào)試cache 2備份.jpg

斷點(diǎn)過 sayNB 斷點(diǎn)到 listenStory


lldb調(diào)試cache 2備份 2.jpg

斷過listenStory 斷到 NSLog
lldb調(diào)試cache 2備份 3.jpg

帶著上面的疑問 我們開始分析源碼 mask 是啥 occupied是啥 為啥變化 ,為啥數(shù)據(jù)會丟失白热, cache到底怎么存儲的摸袁?

底層源碼分析

cache_t 中找線索


struct cache_t {
....省略
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();

    unsigned capacity();
    bool isConstantEmptyCache();
    bool canBeFreed();
....省略

看到了incrementOccupied(); 函數(shù) 中文是 增加 occupied 點(diǎn)進(jìn)去

void cache_t::incrementOccupied() 
{
    _occupied++;
}

_occupied ++ 緩存一個(gè)方法就++ ?繼續(xù)探尋全局搜索 incrementOccupied() 此方法 看從哪里調(diào)用的

ALWAYS_INLINE
void cache_t::insert(Class cls, SEL sel, IMP imp, id receiver)
{ 
 .....省略 后面著重分析
}

我的天在整個(gè)源碼里面 只有這一個(gè)地方進(jìn)行了 調(diào)用 看方法名字為insert 在繼續(xù)搜索 insert 看在哪里調(diào)用的

void cache_fill(Class cls, SEL sel, IMP imp, id receiver)
{
    runtimeLock.assertLocked();

#if !DEBUG_TASK_THREADS
    // Never cache before +initialize is done
    if (cls->isInitialized()) {
        cache_t *cache = getCache(cls);
#if CONFIG_USE_CACHE_LOCK
        mutex_locker_t lock(cacheUpdateLock);
#endif
        cache->insert(cls, sel, imp, receiver);
    }
#else
    _collecting_in_critical();
#endif
}

繼續(xù)搜索cache_fill,發(fā)現(xiàn)在寫入之前,還有一步操作,即cache讀取漾岳,即查找sel-imp,如下圖


截屏2020-09-19 下午1.21.25.png

insert 方法分析

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   /////如果緩存不足3/4滿,就按原樣使用
    mask_t newOccupied = occupied() + 1;/// newOccupied = ( 拿到 以前的 occuoied() +1 ),如沒有屬性賦值家乘,occupied() = 0
    unsigned oldCapacity = capacity(), capacity = oldCapacity; //   return mask() ? mask()+1 : 0;
    if (slowpath(isConstantEmptyCache())) {   ///判斷是否需要初始化創(chuàng)建緩存  小概率:occupied() = 0 時(shí)
        // Cache is read-only. Replace it.
        if (!capacity) capacity = INIT_CACHE_SIZE; ///  初始化 capacity =  (1<<2)  二進(jìn)制 100   十進(jìn)制 4
        reallocate(oldCapacity, capacity, /* freeOld */false); /// 開辟 空間  不需要釋放回收老的內(nèi)存
    }
    else if (fastpath(newOccupied + CACHE_END_MARKER <= capacity / 4 * 3)) {
        // Cache is less than 3/4 full. Use it as-is.  //緩存不足3/4滿蝗羊。按原樣使用它。
//        假如上之前有兩個(gè)緩存
        
//        mask_t newOccupied = occupied() + 1; ///2 +1
        
        //第一次開辟 申請內(nèi)存是4個(gè)  已經(jīng)有2個(gè)插入 bucket 插入到緩存里
         /// newOccupied + 1 < = capacity/4*3  == (3+1 <= capacity/4*3)所以不滿足 要進(jìn)行內(nèi)容擴(kuò)張 看下面的方法
    }
    
    else {
        /// 有 cap 是否 存才 : 存在 進(jìn)行 2倍擴(kuò)容 :不存在 4
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
        /// 最大 不能 超過   1<<16
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
        ///重新開辟內(nèi)存空間 并回收老的數(shù)據(jù)
        reallocate(oldCapacity, capacity, true);
    }

    bucket_t *b = buckets();/// 獲取 bukets
    mask_t m = capacity - 1;  //mask    實(shí)際內(nèi)存?zhèn)€數(shù) -1 類似 最大 下標(biāo)
   /// 獲取 根據(jù)  m 7 和當(dāng)前 sel  獲取 hash表 mask
    mask_t begin = cache_hash(sel, m);  //查找 hash表 key為 sel  m = 最大下標(biāo)   計(jì)算當(dāng)前需要插入的緩存下標(biāo)
    mask_t i = begin;  //i = 0

    // 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.
    //掃描第一個(gè)未使用的插槽并插入仁锯。
    //保證有一個(gè)空槽耀找,因?yàn)?    //最小尺寸是4,我們將大小調(diào)整為3/4滿业崖。
    
    do {
        ///循環(huán)遍歷 buckets() sel() 一旦發(fā)現(xiàn) 沒有 就進(jìn)行 occupied+1 并進(jìn)行存儲 并跳出循環(huán)
        if (fastpath(b[i].sel() == 0)) {
            incrementOccupied();
            b[i].set<Atomic, Encoded>(sel, imp, cls);
            return;
        }
        ///循環(huán)遍歷 發(fā)現(xiàn)了已經(jīng)有存了 occupied 不做任何處理 并return
        if (b[i].sel() == sel) {
            // The entry was added to the cache by some other thread
            // before we grabbed the cacheUpdateLock.
            return;
        }
   
  
  ///解決哈希沖突 重新獲取新的哈希下標(biāo)
    } while (fastpath((i = cache_next(i, m)) != begin));
    
     

    cache_t::bad_cache(receiver, (SEL)sel, cls);
}

分析:

  • 第一步野芒,根據(jù)occupied的值計(jì)算出當(dāng)前的緩存占用量,當(dāng)沒有方法調(diào)用時(shí)候 _occupied 為0
mask_t cache_t::occupied() 
{
    return _occupied;
}
  • 第二步双炕,根據(jù)緩存占用量計(jì)算需開辟空間大小
    1.是否為初始化 首次 開辟空間 是的話 開辟 4 個(gè)大小

    if (slowpath(isConstantEmptyCache())) {   ///判斷是否需要初始化創(chuàng)建緩存  小概率:occupied() = 0 時(shí)
        // Cache is read-only. Replace it.
        if (!capacity) capacity = INIT_CACHE_SIZE; ///  初始化 capacity =  (1<<2)  二進(jìn)制 100   十進(jìn)制 4
        reallocate(oldCapacity, capacity, /* freeOld */false); /// 開辟 空間  不需要釋放回收老的內(nèi)存
    }

2.如果緩存占用量小于等于3/4狞悲,則不作任何處理

   else if (fastpath(newOccupied + CACHE_END_MARKER <= capacity / 4 * 3)) {
        // Cache is less than 3/4 full. Use it as-is.  //緩存不足3/4滿。按原樣使用它妇斤。
//        假如上之前有兩個(gè)緩存
        
//        mask_t newOccupied = occupied() + 1; ///2 +1
        
        //第一次開辟 申請內(nèi)存是4個(gè)  已經(jīng)有2個(gè)插入 bucket 插入到緩存里
         /// newOccupied + 1 < = capacity/4*3  == (3+1 <= capacity/4*3)所以不滿足 要進(jìn)行內(nèi)容擴(kuò)張 看下面的方法
    }
    

3.如果緩存占用量超過3/4摇锋,則需要進(jìn)行兩倍擴(kuò)容以及重新開辟空間

     /// 有 cap 是否 存才 : 存在 進(jìn)行 2倍擴(kuò)容 :不存在 4
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
        /// 最大 不能 超過   1<<16
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
        ///重新開辟內(nèi)存空間 并回收老的數(shù)據(jù)
        reallocate(oldCapacity, capacity, true);
    }
  • 第三步,針對需要存儲的bucket進(jìn)行內(nèi)部imp 和sel賦值
 bucket_t *b = buckets();/// 獲取 bukets
    mask_t m = capacity - 1;  //mask    實(shí)際內(nèi)存?zhèn)€數(shù) -1 類似 最大 下標(biāo)
   /// 獲取 根據(jù)  m 7 和當(dāng)前 sel  獲取 hash表 mask
    mask_t begin = cache_hash(sel, m);  //查找 hash表 key為 sel  m = 最大下標(biāo)   計(jì)算當(dāng)前此次插入的哈希下標(biāo)
    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.
    //掃描第一個(gè)未使用的插槽并插入站超。
    //保證有一個(gè)空槽荸恕,因?yàn)?    //最小尺寸是4,我們將大小調(diào)整為3/4滿死相。
    
    do {
        ///循環(huán)遍歷 buckets() sel() 一旦發(fā)現(xiàn) 沒有 就進(jìn)行 occupied+1 并進(jìn)行存儲 并跳出循環(huán)
        if (fastpath(b[i].sel() == 0)) {
            incrementOccupied();
            b[i].set<Atomic, Encoded>(sel, imp, cls);
            return;
        }
        ///循環(huán)遍歷 發(fā)現(xiàn)了已經(jīng)有存了 occupied 不做任何處理 并return
        if (b[i].sel() == sel) {
            // The entry was added to the cache by some other thread
            // before we grabbed the cacheUpdateLock.
            return;
        }
   
  
 ///解決哈希沖突 重新獲取新的哈希下標(biāo)
    } while (fastpath((i = cache_next(i, m)) != begin));
    
     

    cache_t::bad_cache(receiver, (SEL)sel, cls);
}

循環(huán) 查找 當(dāng)前sel在緩存是否存才 腿宰,存在直接跳出循環(huán) 不存在存入緩存并且緩存占用量+1 并跳出循環(huán)

cache原理分析的流程圖

cache_fill方法.jpg

疑問解答

1窃这、_mask是什么吮旅?
_mask是指的掩碼數(shù)據(jù),用于在哈希算法或者哈希沖突算法中計(jì)算哈希下標(biāo)县昂,其中mask = 開辟空間總大小 -1

2、_occupied 是什么陷舅?
_occupied 表示 當(dāng)前存儲了幾個(gè) sel-imp 倒彰,方法調(diào)用 也就是消息發(fā)送 會導(dǎo)致 occupied 變化

3、為什么 調(diào)用第三個(gè)方法的時(shí)候 _mask 會變?yōu)?7 _occupied 變?yōu)榱?code>1 蔑赘?
因?yàn)樵?code>cache初始化的時(shí)候狸驳,分配的空間是4個(gè),隨著方法的增多缩赛,當(dāng)存儲的 sel-imp個(gè)數(shù) 即newOccupied + CACHE_END_MARKER(等于1)的和 超過 總?cè)萘康?/4,例如有4個(gè)時(shí)耙箍,當(dāng)occupied等于2時(shí),就需要對cache的內(nèi)存進(jìn)行兩倍擴(kuò)容

4酥馍、為什么 數(shù)據(jù)丟失了呢 辩昆?
因?yàn)?code>sel-imp的存儲是通過哈希算法計(jì)算下標(biāo)的,其計(jì)算的下標(biāo)有可能已經(jīng)存儲了sel被占用旨袒,所以又需要通過哈希沖突算法重新計(jì)算哈希下標(biāo)汁针,所以導(dǎo)致下標(biāo)隨機(jī)的,并不是固定的

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末砚尽,一起剝皮案震驚了整個(gè)濱河市施无,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌必孤,老刑警劉巖猾骡,帶你破解...
    沈念sama閱讀 211,948評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異敷搪,居然都是意外死亡兴想,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,371評論 3 385
  • 文/潘曉璐 我一進(jìn)店門赡勘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來嫂便,“玉大人,你說我怎么就攤上這事闸与”刑妫” “怎么了?”我有些...
    開封第一講書人閱讀 157,490評論 0 348
  • 文/不壞的土叔 我叫張陵践樱,是天一觀的道長厂画。 經(jīng)常有香客問我,道長映胁,這世上最難降的妖魔是什么木羹? 我笑而不...
    開封第一講書人閱讀 56,521評論 1 284
  • 正文 為了忘掉前任甲雅,我火速辦了婚禮解孙,結(jié)果婚禮上坑填,老公的妹妹穿的比我還像新娘。我一直安慰自己弛姜,他們只是感情好脐瑰,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,627評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著廷臼,像睡著了一般苍在。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上荠商,一...
    開封第一講書人閱讀 49,842評論 1 290
  • 那天寂恬,我揣著相機(jī)與錄音,去河邊找鬼莱没。 笑死初肉,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的饰躲。 我是一名探鬼主播牙咏,決...
    沈念sama閱讀 38,997評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼嘹裂!你這毒婦竟也來了妄壶?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,741評論 0 268
  • 序言:老撾萬榮一對情侶失蹤寄狼,失蹤者是張志新(化名)和其女友劉穎丁寄,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體例嘱,經(jīng)...
    沈念sama閱讀 44,203評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡狡逢,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,534評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了拼卵。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片奢浑。...
    茶點(diǎn)故事閱讀 38,673評論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖腋腮,靈堂內(nèi)的尸體忽然破棺而出雀彼,到底是詐尸還是另有隱情,我是刑警寧澤即寡,帶...
    沈念sama閱讀 34,339評論 4 330
  • 正文 年R本政府宣布徊哑,位于F島的核電站,受9級特大地震影響聪富,放射性物質(zhì)發(fā)生泄漏莺丑。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,955評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望梢莽。 院中可真熱鬧萧豆,春花似錦、人聲如沸昏名。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,770評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽轻局。三九已至洪鸭,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間仑扑,已是汗流浹背览爵。 一陣腳步聲響...
    開封第一講書人閱讀 32,000評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留镇饮,地道東北人拾枣。 一個(gè)月前我還...
    沈念sama閱讀 46,394評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像盒让,于是被迫代替她去往敵國和親梅肤。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,562評論 2 349