objc_class中的cache_t分析

本文探索的的主要是兩點

1、cache_t的結(jié)構(gòu)

2、cache_t里存儲的哪些

cache_t結(jié)構(gòu)分析

打開源碼,點進(jìn)cache_t中查看cache_t的底層代碼

  • 便于分析,暫時剔除去里面的static等靜態(tài)變量
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
    explicit_atomic<struct bucket_t *> _buckets;
    explicit_atomic<mask_t> _mask;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    explicit_atomic<uintptr_t> _maskAndBuckets;
    mask_t _mask_unused;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    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;

public:
//省略方法
  • 首先搞清楚這里的用來做判斷條件的宏是什么意思
#if defined(__arm64__) && __LP64__//真機(jī)(64位)
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16
#elif defined(__arm64__) && !__LP64__//真機(jī)(非64位)
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_LOW_4
#else
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_OUTLINED//模擬器或者macOS
#endif
  • 以上代碼大致可以cache_t所包含的內(nèi)容:
    非真機(jī)端:_buckets_maskflags_occupied
    真機(jī)端:_maskAndBucketsflags_occupied
    注:真機(jī)端時,_maskAndBuckets ,編譯器為了優(yōu)化,將_buckets_mask 合并

-再看下_buckets包含哪些

struct bucket_t {
private:
#if __arm64__
    explicit_atomic<uintptr_t> _imp;
    explicit_atomic<SEL> _sel;
#else
    explicit_atomic<SEL> _sel;
    explicit_atomic<uintptr_t> _imp;
#endif
  • _buckets包含了_imp_sel,真機(jī)和非真機(jī)只是imp和sel的順序不一樣

  • 至此我們可以得出cache_t內(nèi)包含的的就是_buckets_maskflags_occupied

下面我們分析cache-t是怎么緩存imp-sel以及 flags_occupied的含義

cache_t流程分析

1、源碼環(huán)境下分析

在person類里創(chuàng)建多個方法

@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__);
}
@end

main文件里調(diào)用方法

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        LGPerson *p  = [LGPerson alloc];
        Class pClass = [LGPerson class];
        [p sayHello];
        [p sayCode];
        [p sayMaster];

[p sayHello]處打斷點,打印結(jié)果如下

執(zhí)行到sayHello時

往下走,執(zhí)行完[p sayHello]后,發(fā)現(xiàn)

執(zhí)行完sayHello

我們發(fā)現(xiàn)_occupied 值發(fā)生變化,由0->1了,可以得出,_occupied占用位置的意義,并且我門發(fā)現(xiàn)imp也有值了,

我們來看一下,sel-IMP內(nèi)容
我們知道sel-imp存在于bucket里,那我們就在bucket里找獲取 sel-imp的函數(shù)

讀取bucket

到bucket里
image.png

下面我們獲取selimp

image.png

buckets是一個數(shù)組,上面操作實際的獲取的buckets中的第一個元素,我們繼續(xù)往下走,看是否是打印出第二個的方法

image.png

可以看出,cahe_t中存儲了運(yùn)行完的方法

  • 下面我們繼續(xù)走完所有的方法

首先看下cache_t的情況


image.png

maskoccpupied都發(fā)生了變化,

再打印看下impsel的情況

image.png

只存儲了最后一個方法

現(xiàn)在我們再添加一些方法,試試添加屬性。看是否被存儲


image.png

數(shù)組中只有1、2、3有值,且2、3順序并不是代碼中方法的執(zhí)行順序

至此可以得出一些奇怪的現(xiàn)象
  • 1、occupiedmask變化,且既不是遞增也不是遞減的變化,是按照什么規(guī)則變化?mask代表什么?
  • 2、selimp丟失了,為什么?
  • 3、方法的存儲順序和執(zhí)行順序不一致
下面我們就著重分析這三個疑問,為了便于打印,我們可以將源碼的數(shù)據(jù)類型復(fù)制進(jìn).m文件中

2、脫離源碼環(huán)境下分析

注意點:

  • 只要保留好底層結(jié)構(gòu)、剔除無用的代碼
    1、需要。所有OC類都是以底層objc_class為模板創(chuàng)建的,所以我們可以直接將任何類強(qiáng)轉(zhuǎn)為wl_objc_class。 在進(jìn)行結(jié)構(gòu)讀取,因為沒有繼承來的objc_class,所以結(jié)構(gòu)中要加上ISA
    2、需要cache_t,進(jìn)去cache_t源碼里精煉出結(jié)構(gòu),其中 explicit_atomic(原子性,用于多線程操作時,數(shù)據(jù)的安全優(yōu)化)可以去除
    3、需要buckets,包含imp和sel
    4、需要mask,進(jìn)去查看,mask本質(zhì)是uint32_t

  • 最終提煉出最終的需要的類型結(jié)構(gòu)

truct wl_bucket_t {
    SEL _sel;
    IMP _imp;
};

struct wl_cache_t {
    struct wl_bucket_t * _buckets;
    mask_t _mask;
    uint16_t _flags;
    uint16_t _occupied;
};

struct wl_class_data_bits_t {
    uintptr_t bits;
};

struct wl_objc_class {
    Class ISA;
    Class superclass;
    struct wl_cache_t cache;             // formerly cache pointer and vtable
    struct wl_class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
};
  • 加入到代碼中使用
typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits

struct wl_bucket_t {
    SEL _sel;
    IMP _imp;
};

struct wl_cache_t {
    struct wl_bucket_t * _buckets;
    mask_t _mask;
    uint16_t _flags;
    uint16_t _occupied;
};

struct wl_class_data_bits_t {
    uintptr_t bits;
};

struct wl_objc_class {
    Class ISA;
    Class superclass;
    struct wl_cache_t cache;             // formerly cache pointer and vtable
    struct wl_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 wl_objc_class *wl_pClass = (__bridge struct wl_objc_class *)(pClass);
        NSLog(@"%hu - %u",wl_pClass->cache._occupied,wl_pClass->cache._mask);
        for (mask_t i = 0; i<wl_pClass->cache._mask; i++) {
            // 打印獲取的 bucket
            struct wl_bucket_t bucket = wl_pClass->cache._buckets[i];
            NSLog(@"%@ - %p",NSStringFromSelector(bucket._sel),bucket._imp);
        }

        
        NSLog(@"Hello, World!");
  • 打印顯示
    image.png

以上,可以更清晰感受出occupiedmask的意義、sel-imp打印順序和調(diào)用順序不匹配的問題以及相關(guān)bucket丟失的問題

  • 1、occupied:當(dāng)前在緩存中的方法占有空間
  • 2、mask:整個緩存所擁有總空間
  • 3、丟失了一些緩存的方法以及方法插入的位置不是原順序

下面我們帶著這些疑問像cachet原理探索

cache_t原理分析

步驟:
一、尋找影響occupied 和mask值的函數(shù)
二、緩存空間是如何分配的

步驟一:

  • 在cachet里,我們發(fā)現(xiàn)有以下關(guān)于occupied函數(shù)


    image.png

字面意思是occupied的增加方法,查看該函數(shù)的實現(xiàn)

void cache_t::incrementOccupied() //occupied自增
{
    _occupied++;
}

繼續(xù)在源碼中搜索在哪里調(diào)用此函數(shù)


image.png
  • insert方法調(diào)用了occupied自增函數(shù),insert可以理解為緩存的插入,即 sel-imp插入緩存的函數(shù)

下面即insert全局搜索

image.png

發(fā)現(xiàn)關(guān)于cache中的insert函數(shù)在cache_fill中也被調(diào)用

  • 全局搜索cache_fill
    image.png

發(fā)現(xiàn)在插入cache前,先讀取了cache,即先sel-imp從緩存中讀取,然后再將sel-imp寫入緩存中。
這個在下面章節(jié)(消息發(fā)送)中探索,先查看插入函數(shù)是怎么操作的

  • 回到insert函數(shù)里
image.png

以上分為三個步驟
【一】計算occupied:即當(dāng)前所占緩存大小,當(dāng)沒有調(diào)用屬性set方法時或者init方法時,occupied為0,那么newOccupied=1

mask_t newOccupied = occupied() + 1;

【二】計算緩存所要使用的總空間:
注:其中每一個判讀里都有一個reallocate函數(shù)
查看得知是一個釋放舊空間,獲取新空間的實現(xiàn)函數(shù)

image.png

分析具體分配空間的操作

  • 首先,如果是第一次創(chuàng)建,空間初始值為4
//oldCapacity 和 capacity的初始值都為0
    unsigned oldCapacity = capacity(), capacity = oldCapacity;
    if (slowpath(isConstantEmptyCache())) {
        // Cache is read-only. Replace it.
        //初始空間為0時,capacity = INIT_CACHE_SIZE = 1 << 2 = 4
        if (!capacity) capacity = INIT_CACHE_SIZE;
        reallocate(oldCapacity, capacity, /* freeOld */false);
    }
  • 如果緩存空間<= 3/4時,緩存還是4個不變。如:初始時,newOccupied為1,所開辟總空間還是為4
//初始時:1+1 <= 3
    else if (fastpath(newOccupied + CACHE_END_MARKER <= capacity / 4 * 3)) {
        // Cache is less than 3/4 full. Use it as-is.
    }
  • 如果緩存大小 > 3/4時,如:newOccupied為3,所開辟總空間為4*2 = 8
else {
        //如果計數(shù)大于3/4, 就需要進(jìn)行擴(kuò)容操作
        // 如果空間存在,就2倍擴(kuò)容。 如果不存在,就設(shè)為初始值4
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
        //最大擴(kuò)容空間為1<<16 = 2^15
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
        //釋放舊空間,創(chuàng)建新緩存空間(第三個入?yún)閠rue,表示需要釋放舊空間)
        reallocate(oldCapacity, capacity, true);
    }

【三】將sel-imp寫進(jìn)緩存

image.png

  • 首先用cache_hash哈希算法設(shè)置方法首次要插入的位置
static inline mask_t cache_hash(SEL sel, mask_t mask) 
{
    //通過sel & mask(mask = cap -1)
    return (mask_t)(uintptr_t)sel & mask;
}
  • 判斷要插入的位置是否已被占用,如果被占用,即使用哈希沖突算法cache_next重新計算位置
#if __arm__  ||  __x86_64__  ||  __i386__
#define CACHE_END_MARKER 1
static inline mask_t cache_next(mask_t i, mask_t mask) {
    //非真機(jī)以及老的arm真機(jī)環(huán)境下,向后走一位。將當(dāng)前的下標(biāo) +1 & mask,重新進(jìn)行哈希計算,得到一個新的下標(biāo)
    return (i+1) & mask;
}

#elif __arm64__
#define CACHE_END_MARKER 0
static inline mask_t cache_next(mask_t i, mask_t mask) {
    //如果i是空,則為mask,mask = cap -1,如果不為空,則 i-1,向前插入sel-imp
    return i ? i-1 : mask;
}
即有三種情況
  • 如果哈希下標(biāo)的位置未存儲sel,即該下標(biāo)位置獲取sel等于0,此時將sel存儲進(jìn)去
  • 當(dāng)前哈希下標(biāo)存儲的sel 等于 即將插入的sel,說明已經(jīng)存儲進(jìn)去了,直接返回
  • 如果當(dāng)前哈希下標(biāo)存儲的sel 不等于 即將插入的sel,則經(jīng)過哈希沖突算法,重新進(jìn)行計算,得到新的下標(biāo),再去對比進(jìn)行存儲
至此,cache_t原理分析完畢,針對于以上的疑問,我們可以得出答案了

1、 mask是掩碼,大小=緩存方法所開辟的總空間大小 - 1,作用是用來和需插入到緩存的sel進(jìn)行&操作,得出sel下標(biāo)
2、 occupied表示哈希表中 sel-imp 的占用大小 (即可以理解為分配的內(nèi)存中已經(jīng)存儲了sel-imp的的個數(shù)),其中init屬性賦值方法調(diào)用都會增加occupied的大小
3、隨著方法的調(diào)用,mask的大小比實際需要大小要大,是因為,當(dāng)目前使用使用的大小+1 > 3/4*總大小時,空間會擴(kuò)容到目前的2倍,假如如目前是2個方法調(diào)用,因為初始空間capacity是4occupied = 2,那么newOccupied = 2+1 = 3newOccupied + CACHE_END_MARKER >= capacity / 4 * 3(其中CACHE_END_MARKER = 1),此時capacity就會擴(kuò)容到8,所以我們只調(diào)用了2個方法時。打印出來的mask已經(jīng)為7
4、方法存儲順序和調(diào)用順序不一致:是因為可能目前空間已經(jīng)>3/4了,需要釋放了舊的空間,重新分配了空間。方法的下標(biāo)使用哈希算法計算得出的,可能之前的下標(biāo)已經(jīng)被其他方法占用,產(chǎn)生了沖突,利用哈希沖突算法重新計算下標(biāo),存儲方法,所以順序是隨機(jī)的。
5、丟失了一些方法:擴(kuò)容時,是將原有的內(nèi)存全部清除了,再重新申請了內(nèi)存導(dǎo)致的

最后,我們可以得出整個cache_t操作流程
后補(bǔ)

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