cache_t 結構解析
在類的底層原理探索 中我們了解了objc_class中存儲了isa,superClass,cache滓技,bits你踩,今天我們來看下cache的作用和底層實現(xiàn)。
cache結構
這個結構并不能看出來cache的作用,所以我們通過內(nèi)存偏移打印一下cache的內(nèi)容
我們打印的信息和其數(shù)據(jù)結構一致击你,但是緩存的內(nèi)容我們還是不知道剃氧,我們只能從源碼中繼續(xù)往下看敏储。
原碼中有一個insert方法,應該是用來存入數(shù)據(jù)的朋鞍。
進入這個insert函數(shù)來看一下
我們注意到insert方法的參數(shù)有SEL,IMP,receiver,并且將這些參數(shù)放在了bucket中已添,我們來驗證一下
bucket_t看到了sel和imp,但是輸出的內(nèi)容又看不懂了滥酥,來看看bucket_t的源碼聲明:
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
// Compute the ptrauth signing modifier from &_imp, newSel, and cls.
uintptr_t modifierForSEL(bucket_t *base, SEL newSel, Class cls) const {
return (uintptr_t)base ^ (uintptr_t)newSel ^ (uintptr_t)cls;
}
// Sign newImp, with &_imp, newSel, and cls as modifiers.
uintptr_t encodeImp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, IMP newImp, UNUSED_WITHOUT_PTRAUTH SEL newSel, Class cls) const {
if (!newImp) return 0;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
return (uintptr_t)
ptrauth_auth_and_resign(newImp,
ptrauth_key_function_pointer, 0,
ptrauth_key_process_dependent_code,
modifierForSEL(base, newSel, cls));
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
return (uintptr_t)newImp ^ (uintptr_t)cls;
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
return (uintptr_t)newImp;
#else
#error Unknown method cache IMP encoding.
#endif
}
public:
static inline size_t offsetOfSel() { return offsetof(bucket_t, _sel); }
inline SEL sel() const { return _sel.load(memory_order_relaxed); }
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
#define MAYBE_UNUSED_ISA
#else
#define MAYBE_UNUSED_ISA __attribute__((unused))
#endif
inline IMP rawImp(MAYBE_UNUSED_ISA objc_class *cls) const {
uintptr_t imp = _imp.load(memory_order_relaxed);
if (!imp) return nil;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
imp ^= (uintptr_t)cls;
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
#else
#error Unknown method cache IMP encoding.
#endif
return (IMP)imp;
}
inline IMP imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls) const {
uintptr_t imp = _imp.load(memory_order_relaxed);
if (!imp) return nil;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
SEL sel = _sel.load(memory_order_relaxed);
return (IMP)
ptrauth_auth_and_resign((const void *)imp,
ptrauth_key_process_dependent_code,
modifierForSEL(base, sel, cls),
ptrauth_key_function_pointer, 0);
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
return (IMP)(imp ^ (uintptr_t)cls);
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
return (IMP)imp;
#else
#error Unknown method cache IMP encoding.
#endif
}
inline void scribbleIMP(uintptr_t value) {
_imp.store(value, memory_order_relaxed);
}
template <Atomicity, IMPEncoding>
void set(bucket_t *base, SEL newSel, IMP newImp, Class cls);
};
我們看到 調用sel()函數(shù)可以返回SEL更舞,調用imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls)可以返回IMP,于是就可以試試在內(nèi)存上獲取到的bucket_t去調用這兩個函數(shù):
cache_t擴容
那方法能存多少呢坎吻?存滿了又是如何擴容呢缆蝉?buckets是如何擴容的?為什么我沒調用class和respondsToSelector方法它們就緩存到了buckets里面了瘦真?
看下cache_t的insert函數(shù)的實現(xiàn)代碼
void cache_t::insert(SEL sel, IMP imp, id receiver)
{
runtimeLock.assertLocked();
// Never cache before +initialize is done
if (slowpath(!cls()->isInitialized())) {
return;
}
if (isConstantOptimizedCache()) {
_objc_fatal("cache_t::insert() called with a preoptimized cache for %s",
cls()->nameForLogging());
}
#if DEBUG_TASK_THREADS
return _collecting_in_critical();
#else
#if CONFIG_USE_CACHE_LOCK
mutex_locker_t lock(cacheUpdateLock);
#endif
ASSERT(sel != 0 && cls()->isInitialized());
// Use the cache as-is if until we exceed our expected fill ratio.
mask_t newOccupied = occupied() + 1; // 第一次insert的時候occupied()即_occupied會是0刊头,newOccupied會是1
// capacity的值就是buckets的長度
unsigned oldCapacity = capacity(), capacity = oldCapacity;
// 如果cache為空,則分配 arm64下長度為2 x86_64下長度為4的buckets诸尽,reallocate里無需釋放老buckets
if (slowpath(isConstantEmptyCache())) {
// Cache is read-only. Replace it.
// 給容量附上初始值原杂,x86_64為4,arm64為2
if (!capacity) capacity = INIT_CACHE_SIZE;
reallocate(oldCapacity, capacity, /* freeOld */false);
}
// 在arm64下您机,緩存的大小 <= buckets長度的7/8 不擴容
// 在x86_64下穿肄,緩存的大小 <= buckets長度的3/4 不擴容
else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) {
// Cache is less than 3/4 or 7/8 full. Use it as-is.
}
#if CACHE_ALLOW_FULL_UTILIZATION // 只有arm64才需要走這個判斷
// 在arm64下,buckets的長度 < = 8 時际看,不擴容
else if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity) {
// Allow 100% cache utilization for small buckets. Use it as-is.
}
#endif
else { // 除卻上面的邏輯被碗,就是擴容邏輯了
// 對當前容量的2倍擴容,并且如果擴容后容量大小 大于 一個最大閾值仿村,則設置為這個最大值
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
// 創(chuàng)建新的擴容后的buckets,釋放舊的bukets
reallocate(oldCapacity, capacity, true);
}
bucket_t *b = buckets(); // 獲取buckets數(shù)組指針
mask_t m = capacity - 1; // m是buckets的長度-1
mask_t begin = cache_hash(sel, m);// 通過hash計算出要插入的方法在buckets上的起始位置(begin不會超過buckets的長度-1)
mask_t i = begin;
// Scan for the first unused slot and insert there.
// There is guaranteed to be an empty slot.
do {
if (fastpath(b[i].sel() == 0)) { // 當前hash計算出來的buckets在i的位置它有沒有值锐朴,如果沒有值就去存方法
incrementOccupied();
b[i].set<Atomic, Encoded>(b, sel, imp, cls());
return;
}
if (b[i].sel() == sel) { // 當前hash計算出來的buckets在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)); // 如果計算出來的起始位置i存在hash沖突的話焚志,就通過cache_next去改變i的值(增大i)
bad_cache(receiver, (SEL)sel);
#endif // !DEBUG_TASK_THREADS
}
從代碼上看衣迷,第一次空了會進行初始化capacity的長度INIT_CACHE_SIZE.
在arm64架構下開辟一個長度為2的桶子,在x86_64架構下開辟長度為4的桶子
else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) {
// Cache is less than 3/4 or 7/8 full. Use it as-is.
}
在arm64架構下如果緩存大于等于桶子長度的7/8酱酬,在x86_64架構下緩存大小大于等于桶子長度的3/4 則什么也不干壶谒。
在arm64架構下,當桶子的長度小于等于8的時候什么也不干
當緩存長度大于(系統(tǒng)設定的)默認最大值就等于默認最大值
其他情況下需要擴容膳沽,擴容的大小為2倍汗菜。
所以我們就明白了為什么之前調用了方法之后會什么沒有找到,arm64下挑社,初始值為2陨界,當?shù)?個方法緩存的時候,則要進行兩倍擴容為4痛阻,并且需要清除舊桶菌瘪。所以instanceMethod在剛進來擴容的時候就被清除掉了,也就找不到了阱当。而前面我們說到class方法和responseToSelector方法是我們在調試的時候通過lldb調用產(chǎn)生的俏扩。