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 & mask對 sel存放位置 index 計算的,so 快取是亂序的。
4、buckets 中方法丟失:因 occupied>2 空間會被重新開闢,舊的空間會被釋放free。
以上。
問題:cache_t::insert 什麼時候呼叫呢?--> 方法呼叫流程 --> objc_msgSend 訊息傳送流後程續文章繼續探索。