1. 程式人生 > 實用技巧 >OC 底層探索 07、類的結構分析2 - cache_t

OC 底層探索 07、類的結構分析2 - cache_t

之前的文章OC底層探索04中,已知如何找到類資訊。本文我們對類資訊中的 cache_t 進行探索。

objc_class 結構 :

OC底層探索04中的指標和記憶體偏移,我們已知可通過指標平移獲取相應位置資訊,cache_t 的位置 = 8 + 8 =16

一、cache_t 簡析

cache_t的原始碼分析:

CACHE_MASK_STORAGE:

1、支援架構

cache_t 原始碼有點長,我們可從擷取的這部分程式碼中看到它對不同架構的支援:

MacOS:i386

模擬器:x86

真機:arm64

cache_t中還可以發現一點,模擬器和真機的一些處理是不同的,業務開發中,我們所除錯使用

的最好的選擇還是真機

2、cache_t 內容

cache_t --> 快取 --> 增刪改查

模擬器:

bucket_t

    explicit_atomic -->我們點選進去可以看到 它是一些 C++ 程式碼,而其中重要的內容是 ‘T’ -->structbucket_t*。關於它,在這裡我們暫時只需要知道它是原子性,為了我們快取的安全性即可,更深層的後續再做探究。

    structbucket_t*imp sel

_mask

真機 64:

從下面程式碼,可觀察到 maskAndBuckets 和一系列帶有‘mask’ 的欄位 --> 掩碼、指標平移

#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16  // 真機64
    explicit_atomic<uintptr_t> _maskAndBuckets;
    mask_t _mask_unused;
    
    // How much the mask is shifted by.
    static constexpr uintptr_t maskShift = 48;
    
    // Additional bits after the mask which must be zero. msgSend
    
// takes advantage of these additional bits to construct the value // `mask << 4` from `_maskAndBuckets` in a single instruction. static constexpr uintptr_t maskZeroBits = 4; // The largest mask value we can store. static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1; // The mask applied to `_maskAndBuckets` to retrieve the buckets pointer. static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << (maskShift - maskZeroBits)) - 1; // Ensure we have enough bits for the buckets pointer. static_assert(bucketsMask >= MACH_VM_MAX_ADDRESS, "Bucket field doesn't have enough bits for arbitrary pointers.");

_maskAndBuckets:-->bucket_t .

_mask_unused:可能是蘋果的預留,不管它 <-- "Don't know how to do ... ..." .

另:

_flags:標記

_occupied:佔位,記憶體佔多少

--> cache_t 結構圖:

二、cache_t 快取了什麼

1、cache_t 快取了方法

執行工程(部分測試程式碼可能存在偏差,可自行編寫),在未呼叫任何方法前,cache_t 內容:

標線所示的值均為 0,繼續執行,p 呼叫方法:

物件 p 呼叫一次方法後,sel imp 不再為0,_mask 3 、occupied+1 -->

推測:方法執行一次後快取在 cache_t 中。(mask occupied 文章後半部分探究)

驗證 _buckets 中存著呼叫過的方法:

cache_t 原始碼中尋找是否有獲取 _buckets 的方法 :

繼續 lldb 除錯:

上圖,可驗證 --> 方法首次執行後快取在 cache_t 中:

cache_t 中sel 就是 物件p剛剛所呼叫方法的方法名

imp 指向是MyPerson中的 方法的指標,指標地址0x0000000100001b50.

2、cache_t 快取集合 - buckets

多個方法呼叫

我們繼續執行程式碼,讓p呼叫方法2:

由上可知:方法呼叫後都會存buckets 中。

同樣通過OC底層探索04的指標和記憶體偏移,通過陣列 index屬性操作:

同樣,我們取到了方法。

思考:方法再呼叫會怎麼樣呢?

方法只會快取一份,方法呼叫的流程是什麼樣子的呢? --> 後續文章再對 objc_msgSend 流程進行探究。

2、cache_t 中 mask 和 occupied 是什麼?

執行工程,呼叫多個方法,進行 lldb 除錯. 如下圖:

除錯過程中,我們發現了幾個問題:

1、occupied 和 mask是什麼?它們的值為何是一直在變化的?

2、cache_t 中方法的順序和呼叫方法順序為何不同

3、buckets 中方法為何丟失不在了?

尋找答案:

1、去 cache_t 原始碼:

進入 mask()occupied() 方法,發現沒什麼有用資訊!

但看到下面incrementOccupied() - occupied 增量:

原始碼中我們發現了 _occupied++mask()的操作.

全域性搜尋 ‘incrementOccupied( ’ --> cache_t 的insert 中做了 occupied/mask 的處理

2、cache_t::insert 方法流程:

 1 ALWAYS_INLINE
 2 void cache_t::insert(Class cls, SEL sel, IMP imp, id receiver)
 3 {
 4 #if CONFIG_USE_CACHE_LOCK
 5     cacheUpdateLock.assertLocked();
 6 #else
 7     runtimeLock.assertLocked();
 8 #endif
 9 
10     ASSERT(sel != 0 && cls->isInitialized());
11 
12     // Use the cache as-is if it is less than 3/4 full
13     mask_t newOccupied = occupied() + 1;// occupied():return _occupied; --> _occupied + 1
14     unsigned oldCapacity = capacity(), capacity = oldCapacity;  // _mask.load() --> capacity 空間
15     if (slowpath(isConstantEmptyCache())) {
16         // 初始化,occupied=0,buckets()是空
17         // Cache is read-only. Replace it.
18         if (!capacity) capacity = INIT_CACHE_SIZE;// capacity 給1<<2的空間 4
19         // 真正去向系統開闢記憶體
20         reallocate(oldCapacity, capacity, /* freeOld */false);
21     }
22     else if (fastpath(newOccupied + CACHE_END_MARKER <= capacity / 4 * 3)) {
23         // Cache is less than 3/4 full. Use it as-is.
24         // cache < 3/4 capacity
25     }
26     else {
27         capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;   // 擴容 如果capacity 不為空 擴容為當前的2倍;為空則去開闢 4
28         if (capacity > MAX_CACHE_SIZE) {// 空間最大 1<<16 = 2^16
29             capacity = MAX_CACHE_SIZE;
30         }
31         reallocate(oldCapacity, capacity, true);// 重新開闢空間,true:舊的free
32     }
33 
34     bucket_t *b = buckets();
35     mask_t m = capacity - 1;// 2^2-1=3  2^3-1=7
36     mask_t begin = cache_hash(sel, m);// sel & mask
37     mask_t i = begin;
38 
39     // Scan for the first unused slot and insert there.
40     // There is guaranteed to be an empty slot because the
41     // minimum size is 4 and we resized at 3/4 full.
42     do {
43         // 位置是空的可以放
44         if (fastpath(b[i].sel() == 0)) {
45             incrementOccupied();
46             b[i].set<Atomic, Encoded>(sel, imp, cls);
47             return;
48         }
49         // 此位置已經存值,且 .sel 就是傳來的這個 sel 了
50         if (b[i].sel() == sel) {
51             // The entry was added to the cache by some other thread
52             // before we grabbed the cacheUpdateLock.
53             return;
54         }
55     } while (fastpath((i = cache_next(i, m)) != begin));// 再次雜湊 --> (i+1)&mask != begin
56 
57     cache_t::bad_cache(receiver, (SEL)sel, cls);
58 }

cache_t::insert邏輯流程概況圖:

從程式碼邏輯流程中,我們可以得到上面問題答案:

1、occupied 從1->2->1->2->3 的原因:當 cache ≥ 3/4capacity 時,空間會重新開闢釋放舊的空間,同時 occupied 手動置0.

2、mask 值變化原因: mask = capacity - 1,所以 它的值是 3 7 15......

3、方法的快取與呼叫順序:快取時通過雜湊演算法:sel & masksel存放位置 index 計算的,so 快取是亂序的。

4、buckets 中方法丟失:因 occupied>2 空間會被重新開闢,舊的空間會被釋放free。

以上。

問題:cache_t::insert 什麼時候呼叫呢?--> 方法呼叫流程 --> objc_msgSend 訊息傳送流後程續文章繼續探索。