前言:
在類的結構分析這篇分析了objc_class
結構體內部的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的讀取有提供相應名稱的方法buckets()
explicit_atomic _buckets;
explicit_atomic _mask;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16 //64位真機
explicit_atomic _maskAndBuckets;//寫在一起的目的是為了優化
mask_t _mask_unused;
//以下都是掩码,即面具 -- 類似于isa的掩碼,即位域
// 掩碼省略....
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4 //非64位 真机
explicit_atomic _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
源碼,可以看到三個架構處理,其中真機架構中的mask和bucket是寫在一起,目的是為了優化,透過不同的架構提供的掩碼
來獲取相應的數據。-
CACHE_MASK_STORAGE_OUTLINED
:運行在macOS或是模擬器 -
CACHE_MASK_STORAGE_HIGH_16
:運行在64位的真機 -
CACHE_MASK_STORAGE_LOW_4
:運行在非64位的真機
-
-
可以看到
cache_t
源碼內部有bucket_t
結構體,內部存放sel
及imp
struct bucket_t { private: #if __arm64__ //真機 //explicit_atomic 是加了原子性的保护 explicit_atomic
_imp; explicit_atomic _sel; #else //非真機 explicit_atomic _sel; explicit_atomic _imp; #endif //方法等其他部分省略 }
查找cache中的sel-imp
cache_t
中查找存儲的sel
-imp
,有以下兩種方式
- 通過源碼
- 脫離源碼環境環境調適分析
事前準備
- 定義一個LGPeron類的聲明
- LGPeron類的實現
- 在main中定義LGPerson類型指針p,並調用其中其中的三個實例方法,在p調用第一個方法第一個方法處加一個斷點。
- ldb查找流程如下
- 透過獲取
pClass
的首地址,偏移16字節後,得到了cache
地址 - 從源碼分析中,我們知道
sel-imp
在cache_t
的_buckets屬性
中,而在cache_t
結構體中提供了獲取_buckets
屬性的方法buckets()
- 獲取了
_buckets
屬性,就可以獲取sel-imp
了,這兩個的獲取在bucket_t
結構體中同樣提供了相應的獲取方法sel()
以及imp(pClass)
- 可以從上面的流程看到,在未執行方法前,緩存是沒有沒有緩存方法的,而在調用方法後,緩存內就有一個緩存。
- 我們現在了解如何獲取
sel-imp
,我們可以透過machOView
來進行確認,將二進制文件拖入machOView
可以在Function段內查看到地址與我們lldb打印出來的地址一樣
- 透過上面的成功獲取了sel-imp,那麼我們接著再打一個斷點在sayCode上,過掉斷點後,執行了sayCode方法,用上面的步驟,先打印cache,可以看到
_occupied = 2
,接著打印buckets以及透過buckets的首地址偏移拿到第二個sel-imp,然後打印第二個sel 及 imp,即可看到結果。
脫離源碼調適分析
- 上述可透過源碼的環境進行lldb動態調適,那麼我們來試試如果不使用源碼環境的狀況下,要如何分析sel-imp呢?
- 其實也就是將所需部分的源碼,拷貝至文件中,完整程式碼如下
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; icache._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
的,我們將源碼拷貝到自己的文件時,就缺少了繼承關係,所以需要聲明這個成員變量,即添加Class ISA
在struct lg_objc_class
中,否則會有下圖之狀況。
- 在lg_objc_class增加Class ISA屬性。
- 增加兩個方法say3 say4。
我們可以從上面的打印結果,發現以下的問題?
- 什麼是
_mask
,_occupied
- 為什麼通過方法調用的增加,打印的
_occupied
和_mask
會變化 -
bucket
數據為什麼丟失情況會有? - 為什麼是say4先打印,say3後打印,明顯順序有問題?為什麼是say4先打印,say3後打印,明顯順序有問題?
- 打印的
cache_t
中的_ocupied
為什麼是從2開始?
Cache_t底層原理分析
- 透過源碼struct cache_t找到incrementOccupied()函數,查看incrementOccupied()具體實現
- incrementOccupied()具體實現如下
- 我們可以在全局環境中搜索incrementOccupied(),看看有哪些地方調用了incrementOccupied(),可以看到只有在cache_t的Insert有調用。
-
cache_t::insert
方法為cache_t
的插入,cache_t
內部儲存的是sel-imp
,以下為從insert方法調用分析,如下為流程圖
insert方法分析
- 以下為詳細註釋
主要分為三個流程區塊
- 流程一:計算當前的緩存佔用量
- 流程二:根據計算後的緩存佔用量進行不同的流程操作
- 流程三:對需要儲存的bucket進行進行內部imp和sel賦值
流程一
mask_t newOccupied = occupied() + 1;
- 當
沒有任何屬性被賦值
以及無方法調用
時,此時occupied為0,而newOccupied就變為1。 - 當
有屬性被賦值
時,會調用set方法,occupied
會增加,當有多個屬性賦值時,就會依照此邏輯增加。 - 當
有方法調用時
,occupied也會增加,當有多個方法調用時,就會依照此邏輯增加。
流程二
- 第一次創建,默認開闢4個(1左移兩位)。
if (slowpath(isConstantEmptyCache())) {
//slowpath發生機率較小,即當occupied()=0時,及創建緩存,創建屬於發生機率較小的事件。
// Cache is read-only. Replace it.
if (!capacity) capacity = INIT_CACHE_SIZE;
//初始化時,capacity = 4(1左移兩位)
reallocate(oldCapacity, capacity, /* freeOld */false);
//開闢空間
//if 的流程為初始化創建
}
- 如果緩存佔用量
小於等於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.
// 如果<=佔用內存的3/4,不做任何事情 確保沒有超過佔用內存的3/4
}
- 如果緩存佔用量
超過3/4
,則需要進行兩倍擴容以及重新開闢空間
else {
//如果超出3/4 則需要擴容(兩倍擴容) 例如occupied為2時,剛好等於,不需擴容
//擴容算法:有capacity,擴容兩倍,沒有capacity進行初始化為4
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE; // 扩容两倍 4
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
//滿了,重新梳理
reallocate(oldCapacity, capacity, true);// 内存 擴容完毕
}
reallocate方法:開闢空間
- 第一次創建以及兩倍擴容時都會使用,源碼如圖所示。
allocateBuckets
方法:向系統開闢bucket,此時的bucket只是一個臨時變量-
setBucketsAndMask
方法:將臨時的bucket存入緩存中,此時的儲存分為為兩種情況:-
如果
是真機
,根據bucket
和mask
的位置儲存,並將occupied佔用設置為0
-
如果
不是真機
,正常儲存bucket
和mask
並將occupied佔用設置為0
-
如果有舊的buckets,需要清理之前的緩存,即調用
cache_collect_free
方法,其源碼實現如下
cache_collect_free
方法實現如下:-
_garbage_make_room
:創建垃圾回收桶
- 如果是
第一次
,需要分配回收空間
- 如果
不是第一次
,則將內存段加大,即原有的兩倍 - 紀錄存儲這次的bucket
- 如果是
cache_collect
方法:垃圾回收,清理舊的bucket
流程三
主要根據
cache_hash
****方法,哈希算法,計算sel-imp
存儲的下標,有以下幾種狀況- 如果哈希下標的位置
未存儲sel
,表示該下標位置獲取sel等於0,將此時sel-imp存儲
進去,並將occupied
佔用加1
- 如果當前哈希下標存儲sel等於即將插入的sel則直接返回。
- 如果當前哈希下標存儲的sel不等於即將插入的sel,則重新通過
cache_next
方法即哈希衝突算法,重新進行哈希計算,得到新的下標,再去對比進行存儲。
其中的
哈希算法
即哈希衝突算法
- 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; //(将当前的哈希下标 +1) & mask,重新进行哈希计算,得到一个新的下标 }#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
是指掩碼數據
,用於在哈希算法
另一哈希冲突算法
中計算哈希下标
,其中mask = capacity - 1
-
_occupied
是什麼?
表示(hash-table)哈希表中
sel-imp
的佔用大小
(分配內存中已經存儲了sel-imp的個數)init
會導致occupied
變化属性赋值
,也會隱式調用,導致occupied
變化方法调用
,導致_occupied
變化為什麼通過方法調用的增加,其打印的
_occupied
和mask会变化
?
因為在
cache
初始化時,分配的空間是4
個,通過方法調用的增量,當存儲的sel-imp個数
,即newOccupied + CACHE_END_MARKER(等於1)的和 超过 總容量的3/4
,例如當occupied
等於2
時,newOccupied就等於3
,上述總和為4
,就需要對cache
的內存進行兩倍擴容
。-
bucket
數據為什麼丢失的情况
會有?,例如2-7中,只有say3,say4方法有函數指針
原因是在
擴容
時,是將原有的內存全部清除
了,再重新申請
了内存
導致的。- 為什麼是say4先打印,say3後打印,明顯
順序
有問題?
因為
sel-imp
的存儲是通過哈希算法計算下標
的,其計算的下標有可能已經存儲了sel
,所以又需要通過哈希衝突重新計算哈希下標
,所以導致下標是隨機的,並不是固定的。- 打印的
cache_t
中的_ocupied
為什麼是從2
開始?
這裡是因為
LGPerson
通過alloc
創建的對象
,並導致兩個屬性賦值的原因,屬性賦值
,會隐式调用set
方法,set方法的調用也會導致occupied
變化。 -