本文旨在經過源碼分析cache_t相關,如緩存策略,動態擴容等,瞭解方法查找的流程。因其與objc_msgSend流程有密切聯繫,而發送消息又是iOS方法的本質,故瞭解cache_t是有必要的。緩存
上一篇iOS底層 -- 類的本質分析中對cache_t結構有初步的瞭解,其底層的結構:bash
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache;
class_data_bits_t bits;
..
}
複製代碼
cache_t
位於objc_class
中函數
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
}
複製代碼
自身的結構由三個屬性組成:源碼分析
buckets
是裝方法的桶子,裏面放着方式實現imp
,根據方法編號sel
生成的key
mask
是擴容因子,同時和key
共同計算得出尋找bucket_t
的哈希索引occupied
是當前佔用的容量在objc源碼中新建CJPerson類,自定義四個方法,在外部對其進行調用post
CJPerson * person = [[CJPerson alloc]init];
Class cls1 = object_getClass(person);
[person firstAction];
Class cls2 = object_getClass(person);
[person secondAction];
Class cls3 = object_getClass(person);
[person thirdAction];
Class cls4 = object_getClass(person);
[person fourthAction];
Class cls5 = object_getClass(person);
複製代碼
在每一個方法調用前輸出下各個cls,以cls1爲例,在[person firstAction]處打斷點 ui
x/4gx
打印
cls1
內存地址的前四位,由於
cache_t
前面的兩個屬性
isa
和
superclass
合佔16字節,故
cache_t
的地址等於
isa
的地址偏移16個字節,便是圖中標註的地址,強轉輸出這個地址獲得
cls1
的
cache_t
內的屬性值,依次類推,輸出其餘的
cls
類對象 | _mask | _occupied |
---|---|---|
cls1 | 3 | 1 |
cls2 | 3 | 2 |
cls3 | 3 | 3 |
cls4 | 7 | 1 |
cls5 | 7 | 2 |
根據這個結果,會發現前面三個cls
還有一點規律可循,occupied
每次+1,但是到cls4
的時候,這個規律又不符合了,而且mask
的值也發生了變化。看來只看打印結果是沒辦法看出是誰在做怪,仍是要去源碼瞧一瞧。this
雖然說從源碼入手這個想法是好的,但是那麼多源碼,究竟要從哪裏看起。思來想去,仍是先搜索下cache_t
, spa
cache_t
是個結構體,那系統應該會 初始化它,而後在操做它,那嘗試搜索下
cache_t *
,
cache_t *getCache(Class cls)
{
assert(cls);
return &cls->cache;
}
複製代碼
這只是個get
方法,排除設計
void cache_t::bad_cache(id receiver, SEL sel, Class isa)
{
// Log in separate steps in case the logging itself causes a crash.
_objc_inform_now_and_on_crash
("Method cache corrupted. This may be a message to an "
"invalid object, or a memory error somewhere else.");
cache_t *cache = &isa->cache;
_objc_inform_now_and_on_crash
("%s %p, SEL %p, isa %p, cache %p, buckets %p, "
"mask 0x%x, occupied 0x%x",
receiver ? "receiver" : "unused", receiver,
sel, isa, cache, cache->_buckets,
cache->_mask, cache->_occupied);
...
}
複製代碼
這是在bad_cache
方法中,看名字和實現,不難判斷出,這是壞緩存的時候發出的警告和崩潰消息調試
static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
cacheUpdateLock.assertLocked();
if (!cls->isInitialized()) return;
if (cache_getImp(cls, sel)) return;
cache_t *cache = getCache(cls);
...
}
複製代碼
這是在cache_fill_nolock
方法中,方法名意爲緩存填充,感受多是這個,但還不能肯定
void cache_erase_nolock(Class cls)
{
cacheUpdateLock.assertLocked();
cache_t *cache = getCache(cls);
...
}
複製代碼
這是在cache_erase_nolock
方法中,方法名意爲緩存擦除
綜上所述,第三條的可能性是最高的,但還須要驗證,在每條方法內打斷點,[person firstAction]調用後,看下執行結果
cache_fill_nolock
內,能夠確認方法調用後,是執行這個方法進行緩存的。
確認了這是方法調用後執行緩存的方法,那就有必要對其實現作深刻的研究了。
static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
cacheUpdateLock.assertLocked(); ①
if (!cls->isInitialized()) return; ②
if (cache_getImp(cls, sel)) return; ③
cache_t *cache = getCache(cls); ④
cache_key_t key = getKey(sel); ⑤
mask_t newOccupied = cache->occupied() + 1; ⑥
mask_t capacity = cache->capacity(); ⑦
if (cache->isConstantEmptyCache()) { ⑧
cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE); ⑨
}
else if (newOccupied <= capacity / 4 * 3) { ⑩
}
else {
cache->expand(); ⑪
}
bucket_t *bucket = cache->find(key, receiver); ⑫
if (bucket->key() == 0) cache->incrementOccupied(); ⑬
bucket->set(key, imp); ⑭
}
複製代碼
① 緩存鎖
② 判斷類是否被初始化,若是未初始化,直接返回
③ 判斷sel是否已經被cls緩存,若是已經緩存就不必再走下面的緩存流程,直接返回
④ 經過cls獲取cache_t的內存地址
⑤ 經過sel強轉生成key
⑥ 獲取cache中當前佔有量occupied + 1的值,給下面進行容量判斷
⑦ 取出cache中的容量capacity
⑧ 判斷當前佔有量是否爲0而且桶子的容量是否爲空的
⑨佔有量爲0而且桶子容量爲空時,進行reallocate從新分配Buckets和Mask
⑩新的佔有量小於等於總容量的3/4時,無另外操做
⑪新的佔有量大於總容量的3/4時,進行expand擴容
⑫ 經過key看成哈希下標尋找對應bucket
⑬ 若是key等於0,說明沒找到,須要緩存,則cache中的當前佔有量 occupied + 1
⑭ 把key和imp裝進桶子bucket
複製代碼
總結:
cache_fill_nolock
先獲取類的cache
,判斷cache
爲空時,建立cache
,若是cache
已經存在,判斷存儲後的佔有量大於容量的3/4時,就擴容。作完這些後,key
和imp
放入bucket
中,把bucket
放入cache
中。
明白了緩存的主流程,在對裏面比較重要的方法,如reallocate
和expand
等作進一步解析
INIT_CACHE_SIZE定義
enum {
INIT_CACHE_SIZE_LOG2 = 2,
INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2)
};
複製代碼
實現
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
{
...
setBucketsAndMask(newBuckets, newCapacity - 1);
if (freeOld) {
cache_collect_free(oldBuckets, oldCapacity);
cache_collect(false);
}
}
複製代碼
能夠看到,在第一次調用時,capacity
爲空,則傳入4,而後給setBucketsAndMask
入參,從新生成_buckets
,而且最後須要丟棄舊的桶子oldBuckets
,清空原來的緩存
void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask)
{
mega_barrier();
_buckets = newBuckets;
mega_barrier();
_mask = newMask;
_occupied = 0;
}
複製代碼
處理完後,_mask
= 4 - 1 = 3,_occupied
= 0。mask = newCapacity - 1,newCapacity = 2^n(初始化最小爲4,n爲擴容次數+2),故mask = 2^n - 1,lldb調試的值得以驗證
void cache_t::expand()
{
cacheUpdateLock.assertLocked();
uint32_t oldCapacity = capacity();
uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;
if ((uint32_t)(mask_t)newCapacity != newCapacity) {
newCapacity = oldCapacity;
}
reallocate(oldCapacity, newCapacity);
}
複製代碼
先獲取oldCapacity
舊容量,
mask_t cache_t::capacity()
{
return mask() ? mask()+1 : 0;
}
複製代碼
等於mask + 1,初始化後mask = 3 ,那這裏oldCapacity
= 4,新容量newCapacity
等於舊容量oldCapacity
* 2 = 8,而後給reallocate
入參,從新分配桶子buckets
和mask
bucket_t * cache_t::find(cache_key_t k, id receiver)
{
assert(k != 0);
bucket_t *b = buckets();
mask_t m = mask();
mask_t i = begin;
do {
if (b[i].key() == 0 || b[i].key() == k) {
return &b[i];
}
} while ((i = cache_next(i, m)) != begin);
Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
cache_t::bad_cache(receiver, (SEL)k, cls);
}
複製代碼
經過cache_hash
函數【begin = k & m】計算出key
值 k
對應的 index
值begin
,用來記錄查詢起始索引,而後把begin
賦值給 i
,用於切換索引。 用這個i
從散列表取值,若是取出來的bucket_t
的 key
= k
,則查詢成功,返回該bucket_t
;若是key
= 0,說明在索引i
的位置上尚未緩存過方法,一樣須要返回該bucket_t
,用於停止緩存查詢。而後while循環其實就至關於繞圈,把散列表頭尾連起來,從begin
值開始,遞減索引值,當走過一圈以後,必然會從新回到begin
值,若是此時尚未找到key
對應的bucket_t
,或者是空的bucket_t
,則循環結束,說明查找失敗,調用bad_cache
方法拋出異常。
1.類的cache_t會對任何方法進行緩存嘛?
類的cache_t
只會緩存對象方法,類方法緩存在元類中
2.爲什方法編號sel須要強轉成key?
方法編號sel是字符串類型,在底層傳遞的效率不如long類型的key
3.mask的做用?
mask
作爲判斷是否擴容的參數之一,能夠稱之爲擴容因子;同時mask
和key
共同生成哈希下表用來查找bucket_t
。這裏的mask
和isa_mask
作爲面具的功能不同,須要進行區分。
4.方法緩存是否有序?
方法緩存的bucket_t
是根據哈希下表獲得的,找到空位子就直接坐下佔有,不存在有序
5.爲何擴容是3/4?
HashMap
的負載因子等於0.75時,空間利用率比較高,並且避免了至關多的Hash衝突,使得底層的鏈表或者是紅黑樹的高度比較低,提高了空間效率。
6.bucket和mask,capacity,sel和imp的關係
bucket
內裝着imp
和sel
生成的key
bucket
是經過key
和mask
進行尋找的bucket
的數量取決於capacity
7.爲何擴容後須要丟棄原先全部的緩存?
感受上,擴容後丟棄緩存,下次調用在從新緩存的行爲使得緩存變的沒有意義。實際上,通常類中有較多方法,若是擴容後,還須要保存原先的方法,新bucket_t
須要從舊的bucket_t
中獲取全部方法,在一個個放入,這自己是比較耗時的行爲。方法緩存的目的就是使消息發送流程更快速,蘋果爲了更快,還使方法查找走彙編流程,總之這一切都是爲了一個字--快。擴容後丟棄原先緩存就是在這個目的下被設計出來,並且這樣設計,從某種意義上來講,也符合LRU
緩存策略,最近被緩存的方法使用率也較高,較早被緩存的使用率較低。
以上就是我對cache_t
的部分理解,cache_t
是消息發送的一個切入點。下一章objc_msgSend
會對cache_t
有一個更宏觀的理解。敬請期待。