在iOS底層探索 --- 類的結(jié)構(gòu)探索(上)中我們分析了cache_t
的大小盯质。今天我們來探索一下cache_t
里面到底存放了些什么宰僧。
1第美、cache_t源碼查看
1.1 源碼簡單分析
首先我們要從源碼中尋找,看看cache_t
到底長什么樣子将谊。
在這里首先要跟打下確認(rèn)幾點(diǎn)內(nèi)容:
-
CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
:表示運(yùn)行的環(huán)境是MacOS
见转,或者是模擬器
管削。。 -
CACHE_MASK_STORAGE_HIGH_16
:表示運(yùn)行的環(huán)境是64位
的真機(jī),一般是指ARM64
架構(gòu)的里伯。 -
CACHE_MASK_STORAGE_LOW_4
:表示非64位
的真機(jī)城瞎,一般指32位
的。 -
CACHE_MASK_STORAGE_OUTLINED
:表示未識別的設(shè)備疾瓮。
我們在閱讀cache_t
源碼的時候,里面有很多內(nèi)容,一時間也看不出來到底有什么用匈织。同樣的盆均,探索的過程終究是比較枯燥的。在漫長的探索過程中肩碟,發(fā)現(xiàn)了這個:bucket_t
為什么是bucket_t
呢强窖?因為我在bucket_t
的定義中發(fā)現(xiàn)了我想要的東西:
正常的緩存,一定要存儲方法的腾务。既然在bucket_t
里面找到了imp
和sel
毕骡;那么說明這條思路是對的,我們順著這條思路繼續(xù)探索岩瘦。
1.2 LLDB打印緩存方法
既然我們大致濾清了cache_t
中方法的存儲形式未巫,那么我們就通過控制臺去打印一下。
我們沿用之前的代碼:
我們的初次LLDB
運(yùn)行到下面階段的時候启昧,遇到了問題叙凡。究竟cache_t
里的緩存方法存在哪里呢?(注意:這里指針平移16字節(jié)
)
上圖中$3
的結(jié)構(gòu)密末,對應(yīng)的就是源碼中的數(shù)據(jù)結(jié)構(gòu):
這里我猜測應(yīng)該是_originalPreoptCache
握爷,存儲著緩存方法。但是在繼續(xù)探索的時候严里,發(fā)現(xiàn)并沒有緩存方法新啼。過程如下:
此時應(yīng)該換一個思路,看一看cache_t
中有沒有一些對應(yīng)的方法刹碾,于是發(fā)現(xiàn)了buckets()
:
這個時候燥撞,我們執(zhí)行以下buckets()
:
到這里我們終于找到了sel
和imp
。但是會發(fā)現(xiàn)迷帜,里面并沒有數(shù)據(jù)物舒,這是因為我們并沒有調(diào)用方法,所以沒有緩存數(shù)據(jù)戏锹。
既然沒有緩存數(shù)據(jù)冠胯,那么我們就執(zhí)行以下方法func
,創(chuàng)造緩存數(shù)據(jù)锦针。但是當(dāng)我們執(zhí)行了方法func
之后荠察,發(fā)現(xiàn)還是沒有數(shù)據(jù)置蜀,不過maybeMask
產(chǎn)生了變化:
這里主要是因為緩存方法的存儲是根據(jù)哈希值來計算下標(biāo)的。我這邊從新執(zhí)行了割粮,然后得到了需要的數(shù)據(jù)盾碗。(哈希值的內(nèi)容,我們文章結(jié)尾再探討)
此時我們可以通過sel()
和imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls)
這兩函數(shù)來獲得具體的sel
和imp
:
-
sel
:
-
imp
:
2 非源碼查看緩存
正常情況下舀瓢,我們從官網(wǎng)獲取的源碼是不能夠編譯的廷雅。有些情況下,我們?nèi)ヅ渲迷创a的時候京髓,也不一定能夠成功讓其編譯通過航缀。(我這邊使用的是命令行工程)
這個時候我們可以采取另外一種方式,讓我們可以繼續(xù)進(jìn)行源碼的探索堰怨。那就是备图,舉個例子如下:
- 拷貝
obj_class
舉個例子灿巧,我們在探索源碼的時候,都要經(jīng)過obj_class
揽涮,所以我們將obj_class
的部分代碼拷貝出來抠藕,修改成我們自己的名字,拷貝的內(nèi)容也是一些屬性等關(guān)鍵信息蒋困。
struct jax_objc_class {
Class isa;
Class superclass;
struct jax_cache_t cache;
struct jax_class_data_bit_t bits;
};
- 整個拷貝之后的代碼如下:
typedef uint32_t mask_t; // x86_64 & arm64 asm are less efficient with 16-bits
struct jax_bucket_t {
SEL _sel;
IMP _imp;
};
struct jax_cache_t {
struct jax_bucket_t *_bukets; // 8
mask_t _maybeMask; // 4
uint16_t _flags; // 2
uint16_t _occupied; // 2
};
struct jax_class_data_bit_t {
uintptr_t bits;
};
struct jax_objc_class {
Class isa;
Class superclass;
struct jax_cache_t cache;
struct jax_class_data_bit_t bits;
};
- 創(chuàng)建
Person
類盾似,并實現(xiàn)一些測試方法:
- 接下來我們在
main
函數(shù)里面檢測一下我們拷貝出來的代碼是否可用。這里我們隨便打印一下cache
里面的信息:
-
由于我們有很多的方法雪标,所以我們可以循環(huán)打印一下
增加方法調(diào)用零院,再次循環(huán)打印村刨;但是當(dāng)我們再次循環(huán)打印的時候告抄,發(fā)現(xiàn)輸出的打印信息不正常:
3 cache_t 底層原理探索
在上面我們調(diào)用多個對象方法的時候,我們的循環(huán)打印發(fā)生了異常嵌牺。
并且還發(fā)現(xiàn)_occupied
和_maybeMask
也發(fā)生了變化打洼。
這究竟是為什么呢?我們還是需要從源碼中尋找答案髓梅。
3.1 occupied
首先關(guān)于occupied
的變化拟蜻,我們發(fā)現(xiàn)了這個函數(shù):void incrementOccupied();
也就是說incrementOccupied()
會讓_occupied
進(jìn)行自加操作绎签。
那么我們就要知道它在哪里別調(diào)用枯饿。
通過搜索發(fā)現(xiàn),它在cache_t
的insert
方法里面被調(diào)用:
3.2 insert
其實在看到insert
方法的時候诡必,我們就應(yīng)該有所感覺了奢方。對應(yīng)緩存搔扁,肯定是要有插入方法的。cache_t
的insert
正是其插入方法蟋字。
接下來我們分析以下insert
源碼:
上面這部分內(nèi)容稿蹲,描述了緩存空間的開辟,其中有一個方法reallocate
值得我們?nèi)パ芯恳幌隆?/p>
因為鹊奖,初始化
和擴(kuò)容
的時候苛聘,都用到了這個方法,但是忠聚,傳入的參數(shù)卻不相同设哗。
-
reallocate
可以看到,開啟緩存空間的方法很簡單两蟀,首先是根據(jù)傳入的值
開辟新的緩存空間
网梢;然后判斷是否有舊的緩存
,如果有就釋放舊的緩存
赂毯。
既然緩存空間已經(jīng)開辟完畢了战虏,那接下來就應(yīng)該是sel
和imp
相關(guān)的操作了。
cache_hask
這個是計算哈希值的函數(shù):
// Class points to cache. SEL is key. Cache buckets store SEL+IMP.
// Caches are never built in the dyld shared cache.
static inline mask_t cache_hash(SEL sel, mask_t mask)
{
uintptr_t value = (uintptr_t)sel;
#if CONFIG_USE_PREOPT_CACHES
value ^= value >> 7;
#endif
return (mask_t)(value & mask);
}
cache_nest
這個是計算哈希沖突的函數(shù):
#if CACHE_END_MARKER
static inline mask_t cache_next(mask_t i, mask_t mask) {
return (i+1) & mask;
}
#elif __arm64__
static inline mask_t cache_next(mask_t i, mask_t mask) {
return i ? i-1 : mask;
}
#else
#error unexpected configuration
#endif
3.3 上面問題解答
我們在上面党涕,調(diào)用多個對象方法的時候烦感,循環(huán)打印出錯了。接著我們探究了源碼中的insert
方法∏补模現(xiàn)在我們可以對這個現(xiàn)象做出解釋了啸盏。
對象方法調(diào)用的增加,
_occupied
和_maybeMask
都變化了
這是因為在cache
初始化的時候骑祟,分配的空間是4個
(INIT_CACHE_SIZE == 4
)回懦;隨著方法調(diào)用的增加,緩存空間不夠用了次企,根據(jù)源碼中的擴(kuò)容算法怯晕,對緩存空間進(jìn)行了兩倍擴(kuò)容。mask
在哈希相關(guān)的函數(shù)中缸棵,我們看到了這個參數(shù)舟茶;這是掩碼
,mask = capacity -1
capacity`是容量的意思堵第。-
_occupied
字面意思理解是占據(jù)吧凉,占位
的意思,可以理解為緩存中已經(jīng)存在的sel-imp
的個數(shù)踏志。
導(dǎo)致_occupied
變化的因素有以下幾個:init
- 屬性賦值
- 方法調(diào)用
上面的循環(huán)打印阀捅,出現(xiàn)
空值
是怎么回事?
這個是緩存空間
重新分配造成的针余,舊的空間
被釋放饲鄙,
新的空間`重新分配凄诞。sel-imp
在緩存中的存儲順序
這一點(diǎn)大家要注意,由于下標(biāo)
是通過哈希計算出來的忍级,所以順序是不固定的帆谍,沒有先后之分。這一點(diǎn)大家可以參考cache_t::insert
函數(shù)的后半部分轴咱。