概要
前面文章我們分析了isa
、bits
,本文主要分析一下cache_t
和類的關系。我們知道cache
是用來緩存指針和函數表的,那么底層是如何具體實現的呢?帶著問題來分析、思考一下。
cache_t的結構
- 首先我們來看一下它的源碼實現
struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
//模擬器或者macOS環境
//explicit_atomic:顯示原子性,保證增刪改查的安全
explicit_atomic<struct bucket_t *> _buckets;//存放SEL、imp
explicit_atomic<mask_t> _mask;
///省略代碼....
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//64位真機環境
explicit_atomic<uintptr_t> _maskAndBuckets;
mask_t _mask_unused;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
//非64位真機環境
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;
//////省略方法.......
};
上面源碼可以看出,cach_t
主要包含_buckets
、_mask
、_flags
、_occupied
四個部分,當然在不同的環境下變量名不同,以上代碼有詳細注釋,我們以MacOS環境為例。
-
我們可以根據cach_t的源碼流程圖來探索一下每個環節的具體實現過程cach_t源碼實現流程圖----來自style_月月簡書
cache_t的結構解釋
-
_buckets
:我們可以進入到bucket_t
源碼查看一下里面的具體實現,我們可以看到無論是arm64還是其他環境下,bucket_t
結構體包含了兩個東西,一個是imp、另外一個是sel。
struct bucket_t {
private:
// IMP-first is better for arm64e ptrauth and no worse for arm64.
// SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
explicit_atomic<uintptr_t> _imp;
explicit_atomic<SEL> _sel;
#else
explicit_atomic<SEL> _sel;
explicit_atomic<uintptr_t> _imp;
#endif
}
-
_mask
:指掩碼數據,用于在哈希算法或者哈希沖突算法中計算哈希下標; -
_flags
:標識 -
_occupied
:表示哈希表中 sel-imp 的占用大小 (即可以理解為分配的內存中已經存儲了sel-imp的的個數);
結合示例代碼分析cache_t是否在方法調用時會被緩存
- 示例代碼
//LGPerson.h
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *lgName;
@property (nonatomic, strong) NSString *nickName;
- (void)sayHello;
- (void)sayCode;
- (void)sayMaster;
- (void)sayNB;
+ (void)sayHappy;
@end
//LGPerson.m
@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__);
}
//mian.m
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
LGPerson *p = [LGPerson alloc];
Class pClass = [LGPerson class];
// p.lgName = @"Cooci";
// p.nickName = @"KC";
// 緩存一次方法 sayHello
// 4
[p sayHello];
[p sayCode];
[p sayMaster];
// [p sayNB];
NSLog(@"%@",pClass);
}
return 0;
}
我們先創建一個類,并添加類方法和實例方法,在main.m
中初始化LGPerson
并調用該方法,來探索cach_t
中的各變量的值
的變化。
- 我們在
[p sayHello];
打個斷點,運行程序,通過lldb
調試看一下cache_t的打印情況:
斷點位置 | 指令 | 輸出結果 |
---|---|---|
[p sayHello] |
p/x pClass |
$0 = 0x0000000100002298 LGPerson |
指針偏移16位 |
0x0000000100002298 + 0x10
|
0x00000001000022a8 |
... ... | p (cache_t *)0x00000001000022a8 |
(cache_t *) $1 = 0x00000001000022a8 |
... ... | p *$1 |
輸出結果見下面代碼 |
`p *$1`的輸出結果
(cache_t) $2 = {
_buckets = {
std::__1::atomic<bucket_t *> = 0x000000010032e420 {
_sel = {
std::__1::atomic<objc_selector *> = (null)
}
_imp = {
std::__1::atomic<unsigned long> = 0
}
}
}
_mask = {
std::__1::atomic<unsigned int> = 0
}
_flags = 32804
_occupied = 0
}
根據表格內容以及輸出結果我們可以得出,在我們沒有調用方法之前,
cache_t
中的bucket_t
,_mask
,_occupied
都沒有值。
- 現在我們過一下
【[p sayHello]】
斷點,使用相同的方法查看一下cache_t
內容
斷點位置 | 指令 | 輸出結果 |
---|---|---|
... ... | p (cache_t *)0x00000001000022a8 |
$5 = 0x00000001000022a8 |
... ... | p *$5 |
輸出結果見下面代碼 |
(cache_t) $6 = {
_buckets = {
std::__1::atomic<bucket_t *> = 0x00000001007bf8c0 {
_sel = {
std::__1::atomic<objc_selector *> = ""
}
_imp = {
std::__1::atomic<unsigned long> = 11928
}
}
}
_mask = {
std::__1::atomic<unsigned int> = 3
}
_flags = 32804
_occupied = 1
}
我們可以看出,在調用了-[LGPerson sayHello]
方法后,_buckets
和_mask
中也有值了_occupied +1
,這就說明當方法被調用后就會被cache
緩存起來。
- 接下來我們證明一下上面輸出結果
(cache_t) $6
中的方法是不是sayHello
,接著上面的步驟我們來打印一下cache_t
中的_sel
和_imp
;
斷點位置 | 指令 | 輸出結果 |
---|---|---|
... ... | $6.buckets() |
$7 = 0x00000001007bf8c0 |
... ... | p *$7 |
輸出結果見下面代碼 |
... ... | p $8.sel() |
(SEL) $9 = "sayHello" |
... ... | p $8.imp(pClass) |
$10 = 0x0000000100000c00 (KCObjc -[LGPerson sayHello])` |
(lldb) p *$7
(bucket_t) $8 = {
_sel = {
std::__1::atomic<objc_selector *> = ""
}
_imp = {
std::__1::atomic<unsigned long> = 11928
}
}
總結:系統在調用方法后確實會被cace_t
緩存起來!那么問題來了,這些cace_t
的值是怎么變化的呢?有什么作用呢?帶著這個問題我們繼續來探索一下。
探索cache
的值的變化
- 我們還是根據前面的斷點接著執行下一個
sayCode
方法,看一下cache
中的值的變化。
斷點位置 | 指令 | 輸出結果 |
---|---|---|
執行完sayCode方法
|
p *$5 |
$11 輸出結果見下面代碼1 |
... ... | p $11.buckets() |
$12 = 0x00000001007bf8c0 |
... ... | p *$12 |
$13 輸出結果見下面代碼2 |
... ... | p $13.sel() |
$14 = "sayHello" |
... ... | 指針偏移1:p *($12 + 1) |
$15 輸出結果見下面代碼3 |
... ... | p $15.sel() |
(SEL) $16 = "sayCode" |
-
$11
輸出結果代碼1
(lldb) p *$5
(cache_t) $11 = {
_buckets = {
std::__1::atomic<bucket_t *> = 0x00000001007bf8c0 {
_sel = {
std::__1::atomic<objc_selector *> = ""
}
_imp = {
std::__1::atomic<unsigned long> = 11928
}
}
}
_mask = {
std::__1::atomic<unsigned int> = 3
}
_flags = 32804
_occupied = 2
-
$13
輸出結果2
(bucket_t) $13 = {
_sel = {
std::__1::atomic<objc_selector *> = ""
}
_imp = {
std::__1::atomic<unsigned long> = 11928
}
}
-
$15
輸出結果3
(bucket_t) $15 = {
_sel = {
std::__1::atomic<objc_selector *> = ""
}
_imp = {
std::__1::atomic<unsigned long> = 11944
}
}
從上面的lldb
的調試表格以及輸出結果我們可以得知一下兩點結論:第一
: 無論什么時候什么方法被調用后都會被cache
緩存起來;第二
:隨著調用方法的數量增多,cache
中的_occupied
也會增加相應的數目。
注意:occupied 是如何遞增的呢?cache又是如何緩存的呢?下面小節分析一下cache_t的底層原理。
cache_t的底層原理分析
前面小節我們發現當有多個方法被調用的時候,cache_t的值就會發生改變,那么是哪個函數引起的呢?在源碼中發現了一個函數incrementOccupied
,這個函數使得occupied
的值進行遞增
void cache_t::incrementOccupied()
{
_occupied++;
}
- 那么這個函數是在什么時候調用的呢?在源碼781中搜索一下,找到了調用這個方法的地方
void cache_t::insert(Class cls, SEL sel, IMP imp, id receiver)
{
#if CONFIG_USE_CACHE_LOCK
cacheUpdateLock.assertLocked();
#else
runtimeLock.assertLocked();
#endif
ASSERT(sel != 0 && cls->isInitialized());
// Use the cache as-is if it is less than 3/4 full
mask_t newOccupied = occupied() + 1;
unsigned oldCapacity = capacity(), capacity = oldCapacity;
if (slowpath(isConstantEmptyCache())) {
// Cache is read-only. Replace it.
if (!capacity) capacity = INIT_CACHE_SIZE;
//向系統申請開辟內存
reallocate(oldCapacity, capacity, /* freeOld */false);
}
//當小于等于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.
}
else {
//超過了3/4進行原來容量的2倍擴容
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE; // 擴容兩倍 4
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
//重新按照擴容后的大小進行開辟空間
reallocate(oldCapacity, capacity, true); // 內存 庫容完畢
}
bucket_t *b = buckets();
mask_t m = capacity - 1;
//計算此次插入的開始的哈希下標
mask_t begin = cache_hash(sel, m);
mask_t i = begin;
// Scan for the first unused slot and insert there.
// There is guaranteed to be an empty slot because the
// minimum size is 4 and we resized at 3/4 full.
/*
通過循環
//掃描第一個未使用的插槽并插入。
//保證有一個空槽,因為
//最小尺寸是4,我們將大小調整為3/4滿。
*/
do {
//如果當前哈希下標的sel未被存儲
if (fastpath(b[i].sel() == 0)) {
//Occupied++
incrementOccupied();
//bucket對sel, imp進行set賦值
b[i].set<Atomic, Encoded>(sel, imp, cls);
return;
}
//如果該下標的位置已經存在,直接返回
if (b[i].sel() == sel) {
// The entry was added to the cache by some other thread
// before we grabbed the cacheUpdateLock.
return;
}
} while (fastpath((i = cache_next(i, m)) != begin));
//循環條件:當前哈希下標存儲的sel不等于即將要插入的sel,通過cache_next方法重新計算得到新的哈希下標。
cache_t::bad_cache(receiver, (SEL)sel, cls);
}
-
cache_t
中insert
流程圖
` insert`流程圖--來自簡書style_月月
流程梳理
- 計算當前的緩存占用數量
mask_t newOccupied = occupied() + 1;
根據當屬性未賦值
及無方法調用
時,此時的occupied()為0
,而newOccupied為1
,
- 第一次進來創建,申請開辟空間;
if (slowpath(isConstantEmptyCache())) { //小概率發生的 即當 occupied() = 0時,即創建緩存,創建屬于小概率事件
// Cache is read-only. Replace it.
if (!capacity) capacity = INIT_CACHE_SIZE; //初始化時,capacity = 4(1<<2 -- 100)
reallocate(oldCapacity, capacity, /* freeOld */false); //開辟空間
//到目前為止,if的流程的操作都是初始化創建
}
關于開辟空間的源碼解析
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld)
{
bucket_t *oldBuckets = buckets();
//向系統申請開辟內存,即開辟bucket,此時的bucket只是一個臨時變量
bucket_t *newBuckets = allocateBuckets(newCapacity);
// Cache's old contents are not propagated.
// This is thought to save cache memory at the cost of extra cache fills.
// fixme re-measure this
ASSERT(newCapacity > 0);
ASSERT((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);
//將newBuckets存入緩存中
setBucketsAndMask(newBuckets, newCapacity - 1);
if (freeOld) {
//如果有舊的oldBuckets,清理之前的緩存
cache_collect_free(oldBuckets, oldCapacity);
}
}
第一步:向系統申請開辟內存
,即開辟bucket
,此時的bucket
只是一個臨時變量
;
第二步:將newBuckets存入緩存
中,如果是真機
,根據bucket和mask的位置存儲
,并將occupied占用設置為0
,如果不是真機
,正常存儲bucket和mask
,并將occupied占用設置為0
。
//真機環境下
_maskAndBuckets.store(((uintptr_t)newMask << maskShift) | (uintptr_t)newBuckets, std::memory_order_relaxed);
_occupied = 0;
//模擬器環境下
_maskAndBuckets.store(buckets | maskShift, memory_order::memory_order_relaxed);
_occupied = 0;
第三步:如果有舊的buckets
,需要清理之前的緩存
,即調用cache_collect_free
方法,其源碼實現如下
if (freeOld) {
//如果有舊的oldBuckets,清理之前的緩存
cache_collect_free(oldBuckets, oldCapacity);
}
//cache_collect_free方法的具體實現
_garbage_make_room ();//創建垃圾回收空間
garbage_byte_size += cache_t::bytesForCapacity(capacity);
garbage_refs[garbage_count++] = data;//記錄緩存這一次的Bucket
cache_collect(false);//垃圾回收,清理舊的Bucket緩存
- 不是第一次創建判斷當前緩存占用數量,如果
小于等于3/4
不做處理,如果超過了3/4
,對原來的容量
進行兩倍擴容
并重新申請空間
;
//當小于等于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.
}
else {
//超過了3/4進行原來容量的2倍擴容
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE; // 擴容兩倍 4
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
//重新按照擴容后的大小進行開辟空間
reallocate(oldCapacity, capacity, true); // 內存 庫容完畢
}
- 針對需要
存儲的bucket
進行內部的sel和imp賦值
,首先需要計算
此次的插入
的哈希下標
,然后通過do-while循環
找到合適的下標
操作(判斷條件:當前哈希下標存儲的sel不等于即將要插入的sel,通過cache_next方法重新計算得到新的哈希下標。),如果當前的哈希下標為存儲sel
,那么對占用數進行++
,即ocuplied++
;如果下標存在直接返回
;
//計算此次插入的開始的哈希下標
mask_t begin = cache_hash(sel, m);
//具體實現
static inline mask_t cache_hash(SEL sel, mask_t mask)
{
return (mask_t)(uintptr_t)sel & mask;
}
//do--while實現
do {
//如果當前哈希下標的sel未被存儲
if (fastpath(b[i].sel() == 0)) {
//Occupied++
incrementOccupied();
//bucket對sel, imp進行set賦值
b[i].set<Atomic, Encoded>(sel, imp, cls);
return;
}
//如果該下標的位置已經存在,直接返回
if (b[i].sel() == sel) {
// The entry was added to the cache by some other thread
// before we grabbed the cacheUpdateLock.
return;
}
} while (fastpath((i = cache_next(i, m)) != begin));
//循環條件:當前哈希下標存儲的sel不等于即將要插入的sel,通過cache_next方法重新計算得到新的哈希下標。