在objective-c語言中,對象調用方法以後,這個方法是會被緩存起來的。下次再調用這個方法的時候,直接從緩存裏面去找,而不用再去遍歷從類到父類再到祖宗類的方法列表了。本文就是從源碼分析這個方法緩存的功能是如何實現的。objective-c
其實就是一個開放地址法的hash表數組
typedef struct objc_class *Class;
struct objc_class : objc_object {
// 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
...
}複製代碼
Class是指向objc_class的指針,objc_class內部存在一個 cache_t cache;cache就是用來緩存最近調用過的方法的。
緩存
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
};複製代碼
typedef unsigned long uintptr_t;
typedef uintptr_t cache_key_t;
struct bucket_t {
cache_key_t _key;
MethodCacheIMP _imp;
}複製代碼
cache_key_t key = getKey(sel);
cache_key_t getKey(SEL sel)
{
assert(sel);
return (cache_key_t)sel;
}複製代碼
先看緩存中是否已經存在了該方法,若是已經存在,直接return掉,不用再緩存安全
從class中拿到cache列表,將sel轉換爲內存地址。bash
將_key與_mask相與,由於_mask是數組大小-1,因此獲得的結果恰好小於數組的大小。less
若是獲得的位置已經被佔用,則日後尋找,直到找到空的位置,把緩存設置到這個位置。oop
void cache_fill(Class cls, SEL sel, IMP imp, id receiver)
{
// 由於cache_t內部用來儲存的結構其實就是個數組
// 因此操做的時候須要先加個鎖,保證線程安全。
mutex_locker_t lock(cacheUpdateLock);
cache_fill_nolock(cls, sel, imp, receiver);
}
static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
cacheUpdateLock.assertLocked();
// Never cache before +initialize is done
if (!cls->isInitialized()) return;
// Make sure the entry wasn't added to the cache by some other thread
// before we grabbed the cacheUpdateLock.
// 若是緩存中已經緩存過了,不用再緩存,直接return
if (cache_getImp(cls, sel)) return;
cache_t *cache = getCache(cls);
cache_key_t key = getKey(sel);
// Use the cache as-is if it is less than 3/4 full
mask_t newOccupied = cache->occupied() + 1;
mask_t capacity = cache->capacity();
if (cache->isConstantEmptyCache()) {
// Cache is read-only. Replace it.
cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
}
else if (newOccupied <= capacity / 4 * 3) {
// Cache is less than 3/4 full. Use it as-is.
}
else {
// Cache is too full. Expand it.
cache->expand();
}
// Scan for the first unused slot and insert there.
// There is guaranteed to be an empty slot because the
// minimum size is 4 and we resized at 3/4 full.
bucket_t *bucket = cache->find(key, receiver);
if (bucket->key() == 0) cache->incrementOccupied();
bucket->set(key, imp);
}
複製代碼
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) {
// mask overflow - can't grow further // fixme this wastes one bit of mask newCapacity = oldCapacity; } reallocate(oldCapacity, newCapacity); } void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity) { bool freeOld = canBeFreed(); 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);
setBucketsAndMask(newBuckets, newCapacity - 1);
if (freeOld) {
cache_collect_free(oldBuckets, oldCapacity);
cache_collect(false);
}
}
bucket_t * cache_t::find(cache_key_t k, id receiver)
{
assert(k != 0);
bucket_t *b = buckets();
mask_t m = mask();
mask_t begin = cache_hash(k, m);
mask_t i = begin;
do {
if (b[i].key() == 0 || b[i].key() == k) {
return &b[i];
}
} while ((i = cache_next(i, m)) != begin);
// hack
Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
cache_t::bad_cache(receiver, (SEL)k, cls);
}複製代碼
cache_t的取出操做爲 cache_getImp(cls, sel) ,該代碼是使用匯編語言編寫的,好在旁邊有註釋。源碼分析
cache_getImp 方法將參數cls 放到r10寄存器,而後調用了 CacheLookup方法
ui
STATIC_ENTRY _cache_getImp
// do lookup
movq %a1, %r10 // move class to r10 for CacheLookup
CacheLookup NORMAL, GETIMP // returns IMP on success複製代碼
將sel放進r11寄存器,而後將 sel和cls->cache.mask相與的結果放進r11寄存器,找到key與現有的sel比較this
.macro CacheLookup
movq %a2, %r11 // r11 = _cmd
andl 24(%r10), %r11d // r11 = _cmd & class->cache.mask
shlq $$4, %r11 // r11 = offset = (_cmd & mask)<<4
addq 16(%r10), %r11 // r11 = class->cache.buckets + offset
cmpq cached_sel(%r11), %a2 // if (bucket->sel != _cmd)
jne 1f // scan more
// CacheHit must always be preceded by a not-taken `jne` instruction
CacheHit $0, $1 // call or return imp
1:
// loop
cmpq $$1, cached_sel(%r11)
jbe 3f // if (bucket->sel <= 1) wrap or miss
addq $$16, %r11 // bucket++
2:
cmpq cached_sel(%r11), %a2 // if (bucket->sel != _cmd)
jne 1b // scan more
// CacheHit must always be preceded by a not-taken `jne` instruction
CacheHit $0, $1 // call or return imp
3:
// wrap or miss
jb LCacheMiss_f // if (bucket->sel < 1) cache miss
// wrap
movq cached_imp(%r11), %r11 // bucket->imp is really first bucket
jmp 2f
// Clone scanning loop to miss instead of hang when cache is corrupt.
// The slow path may detect any corruption and halt later.
1:
// loop
cmpq $$1, cached_sel(%r11)
jbe 3f // if (bucket->sel <= 1) wrap or miss
addq $$16, %r11 // bucket++
2:
cmpq cached_sel(%r11), %a2 // if (bucket->sel != _cmd)
jne 1b // scan more
// CacheHit must always be preceded by a not-taken `jne` instruction
CacheHit $0, $1 // call or return imp
3:
// double wrap or miss
jmp LCacheMiss_f
.endmacro複製代碼