在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就是用来缓存最近调用过的方法的。
cache_t内部结构
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
};复制代码
- _buckets:数组,是bucket_t结构体的数组,bucket_t是用来存放方法的SEL内存地址和IMP的
- _mask的大小是数组大小 - 1,用作掩码。(因为这里维护的数组大小都是2的整数次幂,所以_mask的二进制位000011, 000111, 001111)刚好可以用作hash取余数的掩码。刚好保证相与后不超过缓存大小。
- _occupied是当前已缓存的方法数。即数组中已使用了多少位置。
typedef unsigned long uintptr_t;
typedef uintptr_t cache_key_t;
struct bucket_t {
cache_key_t _key;
MethodCacheIMP _imp;
}复制代码
- _key:cache_key_t 就是 unsigned long类型,用来存储SEL的内存地址。SEL应该是char*类型的字符串,char*强转unsigned long,其实就是SEL的内存地址。代码如下
- _imp就是方法实现IMP了。
cache_key_t key = getKey(sel);
cache_key_t getKey(SEL sel)
{
assert(sel);
return (cache_key_t)sel;
}复制代码
cache_t的存入
先看缓存中是否已经存在了该方法,如果已经存在,直接return掉,不用再缓存
从class中拿到cache列表,将sel转换为内存地址。
- 如果当前cache还没被初始化,则分配一个大小为4的数组,并设置_mask为3。
- 如果存入缓存后的大小小于当前大小的3/4,则当前缓存大小还可以使用,无需扩容
- 否则缓存太满,需要扩容,扩容为原来大小的2倍。放弃旧的缓存,新扩容的缓存为空。
将_key与_mask相与,因为_mask是数组大小-1,所以得到的结果刚好小于数组的大小。
如果得到的位置已经被占用,则往后寻找,直到找到空的位置,把缓存设置到这个位置。
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_t的取出操作为 cache_getImp(cls, sel) ,该代码是使用汇编语言编写的,好在旁边有注释。
cache_getImp 方法将参数cls 放到r10寄存器,然后调用了 CacheLookup方法
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比较
.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复制代码