本文探索的的主要是兩點
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
、_mask
、flags
、_occupied
真機(jī)端:_maskAndBuckets
、flags
、_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
、_mask
、flags
、_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í)行完[p sayHello]
后,發(fā)現(xiàn)
我們發(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
下面我們獲取sel
和imp
buckets是一個數(shù)組,上面操作實際的獲取的buckets中的第一個元素,我們繼續(xù)往下走,看是否是打印出第二個的方法
image.png
可以看出,cahe_t
中存儲了運(yùn)行完的方法
- 下面我們繼續(xù)走完所有的方法
首先看下cache_t的情況
image.png
mask
、occpupied
都發(fā)生了變化,
再打印看下
imp
、sel
的情況
image.png
只存儲了最后一個方法
現(xiàn)在我們再添加一些方法,試試添加屬性。看是否被存儲
image.png
數(shù)組中只有1、2、3有值,且2、3順序并不是代碼中方法的執(zhí)行順序
至此可以得出一些奇怪的現(xiàn)象
- 1、
occupied
和mask
在變化
,且既不是遞增也不是遞減的變化,是按照什么規(guī)則變化?mask代表什么? - 2、
sel
和imp丟失
了,為什么? - 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
以上,可以更清晰感受出occupied
和mask
的意義、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ù)
-
insert
方法調(diào)用了occupied自增函數(shù),insert
可以理解為緩存的插入,即sel-imp
插入緩存的函數(shù)
下面即insert
全局搜索
發(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ù)里
以上分為三個步驟
【一】計算occupied
:即當(dāng)前所占緩存大小,當(dāng)沒有調(diào)用屬性set方法時或者init方法時,occupied為0,那么newOccupied=1
mask_t newOccupied = occupied() + 1;
【二】計算緩存所要使用的總空間:
注:其中每一個判讀里都有一個reallocate
函數(shù)
查看得知是一個釋放舊空間,獲取新空間的實現(xiàn)函數(shù)
分析具體分配空間的操作
- 首先,如果是第一次創(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)緩存
- 首先用
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是4
,occupied = 2
,那么newOccupied = 2+1 = 3
,newOccupied + 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ǔ)