前言:
在類的結(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)部存放sel
及imp
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-imp
在cache_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_class
的ISA
屬性是繼承自objc_object
的,我們將源碼拷貝到自己的文件時恕出,就缺少了繼承關(guān)係询枚,所以需要聲明這個成員變量,即添加Class ISA
在struct 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ù)bucket
和mask
的位置儲存,並將occupied佔(zhàn)用設(shè)置為0
-
如果
不是真機(jī)
瘪匿,正常儲存bucket
和mask
並將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)用的增加肮柜,其打印的
_occupied
和mask會變化
?
因為在
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
變化岸售。 -