1. cache 中存儲的是什么?
在 objc_class結構體中,有 cache 這個成員,而且還是一個結構體類型
:
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
...
我們直接看cache_t
的源碼:
struct cache_t {
//macOS、模擬器 -- 主要是架構區分
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
// explicit_atomic 顯示原子性,目的是為了能夠 保證 增刪改查時 線程的安全性
// 方法的緩存數組(以散列表的形式存儲 bucket_t,用來存儲sel imp)
explicit_atomic<struct bucket_t *> _buckets;
// _buckets 的數組長度-1,容量的臨界值
explicit_atomic<mask_t> _mask;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16 // 64位真機
// 掩碼和 Buckets 指針共同保存在 uintptr_t 類型的 _maskAndBuckets中
explicit_atomic<uintptr_t> _maskAndBuckets;
// 未使用的容量
mask_t _mask_unused;
// 高 16 位是 mask 掩碼,即 _maskAndBuckets 右移 48 位得到 mask
static constexpr uintptr_t maskShift = 48;
// 掩碼后的其他位必須為 0
// msgSend 利用這些額外的位,在單個指令中從 _maskAndBuckets 構造了值 mask<<4
static constexpr uintptr_t maskZeroBits = 4;
// 我們可以保存的最大的 mask 值
// (64 - maskShift) 即掩碼位數,然后 將 1 左移掩碼位數后再減 1 即 16 位能保存的最大二進制數值
static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1;
// 應用于 _maskAndBuckets 的掩碼,以獲取 buckets 指針
// 1 左移 44(48-4)位后再減 1(44 位 1,其余都是 0 的數值)
static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << (maskShift - maskZeroBits)) - 1;
// 確保我們有足夠的位用于存儲 buckets 指針。
static_assert(bucketsMask >= MACH_VM_MAX_ADDRESS, "Bucket field doesn't have enough bits for arbitrary pointers.");
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4 //非64位 真機
explicit_atomic<uintptr_t> _maskAndBuckets;
mask_t _mask_unused;
// _maskAndBuckets 將掩碼移位存儲在低 4 位中,并將 buckets 指針存儲在該值的其余部分中
#else
#error Unknown cache mask storage type.
#endif
#if __LP64__
// 如果是 64 位環境的話,會多一個 _flags 標志位
uint16_t _flags;
#endif
// 緩存數組的已占用量
uint16_t _occupied;
//方法省略.....
}
我們看到最上面有一個宏判斷,其實是架構的處理
:
#define CACHE_MASK_STORAGE_OUTLINED 1
#define CACHE_MASK_STORAGE_HIGH_16 2
#define CACHE_MASK_STORAGE_LOW_4 3
#if defined(__arm64__) && __LP64__ // 64位真機
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16
#elif defined(__arm64__) && !__LP64__ // 非 64位真機
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_LOW_4
#else
#define CACHE_MASK_STORAGE // 其它 CACHE_MASK_STORAGE_OUTLINED
#endif
我們再來看bucket_t
的源碼,分為兩個版本:真機和非真機,不同的區別只是在于 sel 和 imp 的順序不一致
:
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
... 方法等其他部分
}
所以通過上面兩個結構體可以知道,cache 中緩存的是sel-imp
。
整體結構如下所示:
2. 在 cache 中查找 sel-imp
cache_t中查找存儲的sel-imp,有以下兩種方式:
- 通過源碼查找
- 脫離源碼在項目中查找
2.1 準備工作
創建一個 Person 類,并定義兩個屬性和 5 個實例方法及其實現:
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, copy) NSString *nickName;
- (void)sayHello;
- (void)sayCode;
- (void)sayMaster;
- (void)sayNB;
+ (void)sayHappy;
@end
@implementation Person
- (void)sayHello{
NSLog(@"Person say: %s",__func__);
}
- (void)sayCode{
NSLog(@"Person say: %s",__func__);
}
- (void)sayMaster{
NSLog(@"Person say: %s",__func__);
}
- (void)sayNB{
NSLog(@"Person say: %s",__func__);
}
+ (void)sayHappy{
NSLog(@"Person say: %s",__func__);
}
@end
2.2 通過 lldb 調試和源碼進行查找
在 main 中定義 Person 對象,并調用其中的 3 個實例方法,添加斷點:
- cache的獲取,需要通過 pClass 的
首地址平移 16 個字節
,即首地址+0x10
獲取 cache 的地址 - 從源碼中可得,sel-imp是在 cache_t的
_buckets
屬性中(目前是 macOS 環境),而且 cache_t結構體也提供了獲取_buckets()
屬性的方法buckets() - 在沒有執行方法調用時,此時的 cache 是沒有緩存的,執行了一次方法調用,cache 中就有了一個緩存,即調用一次方法就會緩存一次方法
- 獲取了_buckets屬性,就可以獲取 sel-imp了,這兩個的獲取在bucket_t結構體中同樣提供了相應的獲取方法
sel()
以及imp(pClass)
接著上面的步驟,我們再次調用一個方法,這次我們想要獲取第二個 sel,調試過程如下:
- 第一個調用方法的存儲獲取很簡單,直接通過
_buckets的首地址
調用對應的方法即可 - 獲取第二個bucket_t需要通過_buckets的
首地址進行偏移
,即p*($9+1)
即可獲取第二個bucket_t,如果有多個方法需要獲取,以此類推。
2.3 脫離源碼通過項目查找
脫離源碼環境,就是將所需的源碼的部分拷貝至項目中,其完整代碼如下:
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 {
Person *person = [Person alloc];
Class pClass = [person class];
[person sayHello];
[person sayCode];
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);
}
}
return 0;
}
注:objc_class的
ISA 屬性
是繼承自 objc_object的,但這里去掉了這個關系,所以將 ISA 屬性直接加上了。
輸出結果:
2021-01-10 14:30:07.436173+0800 DebugTest[5488:251965] Person say: -[Person sayHello]
2021-01-10 14:30:07.436624+0800 DebugTest[5488:251965] Person say: -[Person sayCode]
2021-01-10 14:30:07.436683+0800 DebugTest[5488:251965] 2 - 3
2021-01-10 14:30:07.436827+0800 DebugTest[5488:251965] sayHello - 0x2940
2021-01-10 14:30:07.436913+0800 DebugTest[5488:251965] sayCode - 0x2eb0
2021-01-10 14:30:07.436956+0800 DebugTest[5488:251965] (null) - 0x0
再增加兩個方法的調用,其打印結果如下:
2021-01-10 15:12:27.852728+0800 DebugTest[5627:264649] Person say: -[Person sayHello]
2021-01-10 15:12:27.853139+0800 DebugTest[5627:264649] Person say: -[Person sayCode]
2021-01-10 15:12:27.853204+0800 DebugTest[5627:264649] Person say: -[Person sayHello]
2021-01-10 15:12:27.853247+0800 DebugTest[5627:264649] Person say: -[Person sayNB]
2021-01-10 15:12:27.853286+0800 DebugTest[5627:264649] 1 - 7
2021-01-10 15:12:27.853328+0800 DebugTest[5627:264649] (null) - 0x0
2021-01-10 15:12:27.853469+0800 DebugTest[5627:264649] sayNB - 0x2ed8
2021-01-10 15:12:27.853519+0800 DebugTest[5627:264649] (null) - 0x0
2021-01-10 15:12:27.853557+0800 DebugTest[5627:264649] (null) - 0x0
2021-01-10 15:12:27.853594+0800 DebugTest[5627:264649] (null) - 0x0
2021-01-10 15:12:27.853629+0800 DebugTest[5627:264649] (null) - 0x0
2021-01-10 15:12:27.853665+0800 DebugTest[5627:264649] (null) - 0x0
對于這次的打印結果,有以下幾點疑問:
-
_mask
是什么? -
_occupied
是什么? - 為什么隨著方法調用的增多,其打印的 occupied 和 mask 會變化?
- 打印的 bucket 數據為什么只有
sayNB
了?
3. cache_t 底層原理分析
3.1 主要流程介紹
首先,從cache_t中的_mask屬性開始分析,找 cache_t中引起變化的函數,發現了incrementOccupied()函數:
具體實現為:
void cache_t::incrementOccupied()
{
_occupied++;
}
全局搜索incrementOccupied()函數,發現只在 cache_t的insert
方法中有調用:
insert方法就是插入方法了,而 cache 中存儲的就是 sel-imp,所以我們從 insert 方法開始分析,下面是 cache 原理分析的流程圖:
3.2 insert 方法實現
ALWAYS_INLINE
void cache_t::insert(Class cls, SEL sel, IMP imp, id receiver)
{
#if CONFIG_USE_CACHE_LOCK
cacheUpdateLock.assertLocked();
#else
// 加鎖,如果加鎖失敗則執行斷言
runtimeLock.assertLocked(); // 同樣,__objc2__ 下使用 runtimeLock
#endif
// 斷言 sel 不能是 0 且 cls 已經完成初始化
ASSERT(sel != 0 && cls->isInitialized());
// 如果緩存占用少于 3/4 則可以繼續保持原樣使用。
// 記錄新的占用量(舊的占用量加 1)
mask_t newOccupied = occupied() + 1;
// 舊容量
unsigned oldCapacity = capacity(), capacity = oldCapacity;
if (slowpath(isConstantEmptyCache())) { // 很可能為假
// 如果目前是空緩存的話,空緩存只是 static bucket_t **emptyBucketsList 用來占位的,
// 實際并不存儲 bucket_t,我們需要重新申請空間,替換空緩存。
if (!capacity) capacity = INIT_CACHE_SIZE; // 如果 capacity 為 0,則賦值給初始值 4
// 根據 capacity 申請新空間并初始化 buckets、mask(capacity - 1)、_occupied
// 這里還有一個點,由于舊 buckets 是準備的占位的靜態數據是不需要釋放的,
// 所以最后一個參數傳遞的是 false。
reallocate(oldCapacity, capacity, /* freeOld */false);
}
else if (fastpath(newOccupied + CACHE_END_MARKER <= capacity / 4 * 3)) {
// 大部分情況都在這里
// 緩存占用少于等于 3/4 的空間。照原樣使用。
// 小括號里面加了一個 CACHE_END_MARKER
// 是因為在 __arm__ || __x86_64__ || __i386__ 這些平臺下,
// 會在 buckets 的末尾放一個 bucket_t *end,所以這里又加了 1
// 而 __arm64__ 平臺下則不存在這個多 +1
}
else {
// 第三種情況則是需要對散列表空間進行擴容
// 擴大為原始 capacity 的 2 倍
// 且這里的擴容時為了性能考慮是不會把舊的緩存復制到新空間的。
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
// 如果大于 MAX_CACHE_SIZE,則使用 MAX_CACHE_SIZE(1 << 16)
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
// 申請空間并做一些初始化
// 不同與 isConstantEmptyCache 的情況,這里擴容后需要釋放舊的 buckets,
// 所以這里第三個參數傳的是 true,表示需要釋放舊 buckets,而這里它也不是立即釋放的,
// 在舊 buckets 沒有被使用并且收集的舊 buckets 容量已經到達閥值了,
// 則會真正進行內存空間的釋放
reallocate(oldCapacity, capacity, true);
}
// 臨時變量
bucket_t *b = buckets();
// mask = cap -1
mask_t m = capacity - 1;
// 使用 sel 和 _mask 進行哈希計算,取得 sel 的哈希值
mask_t begin = cache_hash(sel, m);
mask_t i = begin;
// 掃描第一個未使用的 "插槽",然后將 bucket_t 插入其中。
// 保證有一個空插槽,因為最小大小為4,
// 且上面已經做過判斷如果使用占比超過 3/4 則進行擴容,
// 且這里的擴容為了性能考慮是不會把舊的緩存復制到新空間的,
// 舊 buckets 會被拋棄,并在合適時候釋放其內存空間
// 這里如果發生哈希沖突的話 do while 會進行一個線性的哈希探測(開放尋址法),
// 為 sel 和 imp 找一個空位。
do {
if (fastpath(b[i].sel() == 0)) {
// 如果 self 為 0,則表示 sel 的哈希值對應的下標處剛好是一個空位置,
// 直接把 sel 和 imp 放在此處即可。
// occupied 已占用數量 +1
incrementOccupied();
// 以原子方式把 sel 和 imp 保存在 Bucket_t 的 _sel 和 _imp 中
b[i].set<Atomic, Encoded>(sel, imp, cls);
return;
}
if (b[i].sel() == sel) {
// 在 cacheUpdateLock(runtimeLock) 加鎖之前,
// 該 sel/imp 已由其他一些線程添加到緩存中。
return;
}
// 下一個哈希值探測,這里不同的平臺不同處理方式依次 +1 或者 -1
} while (fastpath((i = cache_next(i, m)) != begin));
// 如果未找到合適的位置則 bad_cache
cache_t::bad_cache(receiver, (SEL)sel, cls);
}
insert 方法主要分為以下3部分:
- 1.計算出當前的緩存占用量
- 2.根據緩存占用量判斷需要執行的操作
- 3.針對需要存儲的 bucket 進行內部 imp 和 sel 賦值
3.2.1 計算當前的緩存占用量:
根據 occupied
的值計算出當前的緩存占用量,當屬性未賦值以及無方法調用時,此時的 occupied()
為 0,而 newOccupied
為 1,如下所示:
mask_t newOccupied = occupied() + 1;
關于緩存占用量的計算,需要注意:
-
alloc
申請空間時,此時的對象已經創建,如果再調用init
方法,occupied
也會+1
,上面示例中我們沒有調用 init 方法; - 當有屬性賦值時,會隱式調用
set
方法,occupied
也會增加 - 當有方法調用時,
occupied
也會增加
3.2.2 根據緩存占用量判斷執行的操作:
- 1.如果是第一次創建,默認
開辟 4 個
- 2.如果緩存占用量<=當前總量的 3/4,則
不作任何處理
- 3.如果緩存占用量超過 3/4,需要進行
兩倍擴容
,以及重新開辟空間,此時之前的緩存會被釋放,也就是我們上面看到只有 sayNB 一個方法的原因
realloccate 方法(開辟空間):
該方法在第一次創建以及兩倍擴容時,都會使用,其源碼實現如下:
ALWAYS_INLINE
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld)
{
// 一個臨時變量用于記錄舊的散列表
bucket_t *oldBuckets = buckets();
// 為新散列表申請指定容量的空間,此時只是一個臨時變量
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);
// 設置 buckets 和 mask,將臨時的 buckets 和 mask 存入緩存中
setBucketsAndMask(newBuckets, newCapacity - 1);
if (freeOld) {
// 這里不是立即釋放舊的 bukckts,而是將舊的 buckets 添加到存放舊散列表的列表中,以便稍后釋放,注意這里是稍后釋放。
cache_collect_free(oldBuckets, oldCapacity);
}
}
上面reallocate這個方法主要有以下幾步:
-
allocateBuckets
方法:向系統申請開辟內存,即開辟 bucket,此時的 bucket 只是一個臨時變量 -
setBucketsAndMask
方法,將臨時的 bucket 和 mask 存入緩存;
如果是真機,根據 bucket 和 mask 的位置存儲,并將occupied 占用設置為 0
:
image.png
如果不是真機,正常存儲 bucket 和 mask,并將 occupied 占用設置為 0:
image.png - 如果有舊的buckets,則需要清理之前的緩存,即調用 cache_collect_free方法,其源碼實現如下:
static void cache_collect_free(bucket_t *data, mask_t capacity)
{
#if CONFIG_USE_CACHE_LOCK
cacheUpdateLock.assertLocked();
#else
runtimeLock.assertLocked(); // 加鎖,加鎖失敗執行斷言
#endif
// 記錄等待釋放的容量
if (PrintCaches) recordDeadCache(capacity);
// 為 garbage 準備空間,需要時進行擴容,創建垃圾回收空間
_garbage_make_room ();
// 增加 garbage_byte_size 的值
garbage_byte_size += cache_t::bytesForCapacity(capacity);
// 把舊的 buckets 放進 garbage_refs 中,garbage_count 并自增 1
garbage_refs[garbage_count++] = data;
// 嘗試去釋放累積的舊緩存(bucket_t)
cache_collect(false);
}
這個方法主要有以下幾步:
- _garbage_make_room方法,創建垃圾回收空間:
static void _garbage_make_room(void)
{
static int first = 1; // 靜態局部變量,下次進來 first 依然是上次的值
// 第一次需要時創建收集表
if (first)
{
first = 0; // 此處置為 0 后,以后調用 _garbage_make_room 再也不會進到這個 if
// 申請初始空間
// 申請 INIT_GARBAGE_COUNT * sizeof(void *) 字節個空間。
// (malloc 不會對空間進行初始化,會保持申請時的垃圾數據)
garbage_refs = (bucket_t**)malloc(INIT_GARBAGE_COUNT * sizeof(void *));
// 當前 garbage_refs 的容量是 INIT_GARBAGE_COUNT
garbage_max = INIT_GARBAGE_COUNT;
}
// Double the table if it is full
// 如果當前 garbage_refs 中 refs 的數量等于 garbage_max 就對 garbage_refs 擴容為當前的 2 倍
else if (garbage_count == garbage_max)
{
// garbage_refs 擴容為 2 倍
garbage_refs = (bucket_t**)
realloc(garbage_refs, garbage_max * 2 * sizeof(void *));
// 更新 garbage_max 為 2 倍
garbage_max *= 2;
}
}
- 記錄存儲這次的 bucket
- cache_collect方法,釋放累積的舊緩存
3.3.3 針對需要存儲的 bucket 進行內部 imp 和 sel 賦值:
這部分主要是根據 cache_hash
方法,即哈希算法, 計算 sel-imp
存儲的哈希下標,分為以下三種情況:
- 1.如果哈希下標的位置未存儲 sel,即該下標位置獲取 sel 為 null,此時將 sel-imp存儲進去,occupied+1
- 2.如果當前哈希下標存儲的 sel 等于即將插入的 sel,則直接返回
- 3.如果當前哈希下標存儲的sel 不等于 即將插入的sel,則重新經過
cache_next
方法 即哈希沖突
算法,重新進行哈希計算,得到新的下標,再去對比進行存儲
cache_hash 哈希算法:
// Class points to cache. SEL is key. Cache buckets store SEL+IMP.
// 類指向緩存。 SEL 是 key。Cache 的 buckets 中保存 SEL+IMP(即 struct bucket_t)。
// Caches are never built in the dyld shared cache.
// Caches 永遠不會構建在 dyld 共享緩存中。
static inline mask_t cache_hash(SEL sel, mask_t mask)
{
// 覺的 hash 值計算好隨意,就是拿 sel 和 mask 與一下,保證不會越界
return (mask_t)(uintptr_t)sel & mask;
}
cache_next 哈希沖突算法:
這里是發生哈希沖突時,哈希值的移動探測方式在不同的平臺下有不同的處理:
#if __arm__ || __x86_64__ || __i386__
// objc_msgSend has few registers available.
// objc_msgSend 的可用寄存器很少。
// Cache scan increments and wraps at special end-marking bucket.
// 緩存掃描增量包裹在特殊的末端標記桶上。
//(此處應該說的是 CACHE_END_MARKER 是 1 時的 endMarker 的位置在 buckets 首位)
#define CACHE_END_MARKER 1
// i 每次向后移動 1,與 mask,保證不會越界
//(并且是到達 mask 后再和 mask 與操作會是 0 ,此時則從 buckets 的 0 下標處開始,
// 然后再依次向后移動探測直到到達 begin,如果還沒有找到合適位置,那說明發生了內存錯誤問題)
static inline mask_t cache_next(mask_t i, mask_t mask) {
return (i+1) & mask;
}
#elif __arm64__
// objc_msgSend has lots of registers available.
// objc_msgSend 有很多可用的寄存器。
// Cache scan decrements. No end marker needed.
// 緩存掃描減量。無需結束標記。
//(此處說的是 CACHE_END_MARKER 是 0 時,不存在 endMarker 賦值)
#define CACHE_END_MARKER 0
// i 依次遞減
static inline mask_t cache_next(mask_t i, mask_t mask) {
return i ? i-1 : mask;
}
#else
// 未知架構
#error unknown architecture
#endif
到此,cache_t的基本流程分析完成了,現在回答一下上面的幾個問題:
_mask是什么?
_mask是掩碼數據,用在哈希算法或者哈希沖突算法中,其中mask = capacity-1
。_occupied是什么?
_occupied表示哈希表中sel-imp的占用大小
,init
|屬性賦值
|方法調用
,會導致 occupied 變化。為什么隨著方法調用的增多,其打印的 occupied 和 mask 會變化?
在cahce 初始化時分配的空間是4 個
,隨著方法調用的增多,存儲的 sel-imp個數超過總容量的3/4
時,會對 cache 進行兩倍擴容
。打印的 bucket 數據為什么只有 sayNB 了?
在擴容時,重新申請
內存導致。