前面我们去查看了类的结构,其中有个cache_t cache;
字段没有去分析,现在我们就去源码中探寻一下。 cache
顾名思义,就是缓存的意思,它是对曾经调用过的方法进行缓存,为什么要缓存呢?首先我们先说下方法的调用,在我们调用[objc message]
的时候会编译成objc_msgSend(objc,@selector(message))
,这时候就对象就会根据自己的isa
指针去寻找自己的类,在找到类后根据类得到class_rw_t
结构体,然后遍历class_rw_t
中的method_array_t methods
数组,如果找不到则根据superclass
指针去遍历父类的方法列表直到找到为止(类方法同理),如果每次调用方法都去查下的话是一个很浪费资源的做法,所以OC就设计了一个cache
去缓存曾经调用过的方法列表。cache_t
是一个散列表,所以可以使用键值对的方式去读取,这样的效率就肯定比遍历快的多,我们简单看下方法是怎么缓存的
方法调用
首先我们在lookUpImpOrForward
打个断点
_objc_msgSend
后会调用该方法
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
Class curClass;
IMP imp = nil;
Method meth;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
if (cache) {
//1,如果本身有缓存就直接返回缓存中的
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
if (!cls->isRealized()) {
// 2,查看类是否加载,若未加载 直接加载
rwlock_writer_t lock(runtimeLock);
realizeClass(cls);
}
if (initialize && !cls->isInitialized()) {
//3,调用initialize方法
_class_initialize (_class_getNonMetaClass(cls, inst));
}
retry:
runtimeLock.read();
// Try this class iss cache.
//4,在类缓存中查找,如果存在 直接返回
imp = cache_getImp(cls, sel);
if (imp) goto done;
// Try this class is method lists.
//5,在自己的方法列表中查找
meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
//6,如果查找到了,加载进缓存,结束查找
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
// Try superclass caches and method lists.
curClass = cls;
//7,循环在类的父类中查找方法
while ((curClass = curClass->superclass)) {
// Superclass cache.
//8,在父类缓存中查找
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
//9,在父类中找到方法并且不是`_objc_msgForward_impcache`(消息转发)就把方法添加的自身的缓存中,并返回
// Found the method in a superclass. Cache it in this class.
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
else {
// Found a forward:: entry in a superclass.
// Stop searching, but do not cache yet; call method
// resolver for this class first.
break;
}
}
// Superclass method list.
//10,缓存中查不到在父类的方法列表中查找
meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
//11查找到,就把方法添加的自身的缓存中,并返回
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
if (resolver && !triedResolver) {
//12,方法解析
runtimeLock.unlockRead();
_class_resolveMethod(cls, sel, inst);
triedResolver = YES;
goto retry;
}
//13,查找不到直接返回`_objc_msgForward_impcache`(方法转发)
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlockRead();
return imp;
}
复制代码
1,如果本身有缓存就直接返回缓存中的
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
复制代码
2,查看类是否加载,若未加载 直接加载
if (!cls->isRealized()) {
rwlock_writer_t lock(runtimeLock);
realizeClass(cls);//类如果没有加入内存 则加入
}
复制代码
3,调用initialize方法
if (initialize && !cls->isInitialized()) {
_class_initialize (_class_getNonMetaClass(cls, inst));
}
复制代码
由此也能看出
initialize
与类加载是否到内存中无关,只要实现了该方法切第一次调用类就会执行该方法
4,在类缓存中查找,如果存在 直接返回
imp = cache_getImp(cls, sel);
if (imp) goto done;
复制代码
5,在自己的方法列表中查找
6,如果查找到了,加载进缓存,结束查找
meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
复制代码
7,循环在类的父类中查找方法
8,在父类缓存中查找
9,在父类中找到方法并且不是_objc_msgForward_impcache
(消息转发)就把方法添加的自身的缓存中,并返回
while ((curClass = curClass->superclass)) {
// Superclass cache.
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
// Found the method in a superclass. Cache it in this class.
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
else {
// Found a forward:: entry in a superclass.
// Stop searching, but do not cache yet; call method
// resolver for this class first.
break;
}
}
}
复制代码
10,缓存中查不到在父类的方法列表中查找
11,查找到,就把方法添加的自身的缓存中,并返回
while ((curClass = curClass->superclass)) {
meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
复制代码
12,方法解析
if (resolver && !triedResolver) {
//12,方法解析
runtimeLock.unlockRead();
_class_resolveMethod(cls, sel, inst);
// Don't cache the result; we don't hold the lock so it may have
// changed already. Re-do the search from scratch instead.
triedResolver = YES;
goto retry;
}
复制代码
在这个函数中实现了复杂的方法解析逻辑。如果
cls
是元类则会发送+resolveClassMethod
,然后根据lookUpImpOrNil(cls, sel, inst, NO/*initialize*/, YES/*cache*/, NO/*resolver*/)
函数的结果来判断是否发送+resolveInstanceMethod
;如果不是元类,则只需要发送+resolveInstanceMethod
消息。这里调用+resolveInstanceMethod
或+resolveClassMethod
时再次用到了objc_msgSend
,而且第三个参数正是传入lookUpImpOrForward
的那个sel
。在发送方法解析消息之后还会调用lookUpImpOrNil(cls, sel, inst, NO/*initialize*/, YES/*cache*/, NO/*resolver*/)
来判断是否已经添加上sel
对应的IMP
了,打印出结果。
13,查找不到直接返回_objc_msgForward_impcache
(方法转发)
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
复制代码
方法缓存
在上面代码中我们可以看到有调用cache_fill
方法
void cache_fill(Class cls, SEL sel, IMP imp, id receiver)
{
#if !DEBUG_TASK_THREADS
mutex_locker_t lock(cacheUpdateLock);
cache_fill_nolock(cls, sel, imp, receiver);
#else
_collecting_in_critical();
return;
#endif
}
复制代码
又调用了cache_fill_nolock
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 was not added to the cache by some other thread
// before we grabbed the cacheUpdateLock.
//读取缓存 如果存在,直接返回
if (cache_getImp(cls, sel)) return;
//获取缓存表,以`sel`地址为key
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) {
//如果缓存小于3/4
// Cache is less than 3/4 full. Use it as-is.
}
else {
// Cache is too full. Expand it.
//缓存超过3/4 则要重新分配缓存空间
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_t *bucket = cache->find(key, receiver);
//判断当前的位置是否为空,如果是的 则将占用量+1
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 - ca not 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 is 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);
}
}
复制代码
在上述代码中可以看到,重新分配缓存空间是原先空间的2倍,然后后将原先的空间释放了,但是没有将原先的数据存储下来,这是为什么呢?主要因为这个表是个散列表,它获取到的位置是拿key
与mask
去进行&
运算得出的,我们可以在find
方法中查看到
bucket_t * cache_t::find(cache_key_t k, id receiver)
{
assert(k != 0);
bucket_t *b = buckets();
mask_t m = mask();
//根据`key`与`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);
}
//该方法就是进行简单的`&`运算获取,因为是`&`运算,所以得到的结果肯定比`mask`小
static inline mask_t cache_hash(cache_key_t key, mask_t mask)
{
return (mask_t)(key & mask);
}
复制代码
mask其实就是当前最大存储量-1
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
{
bool freeOld = canBeFreed();
bucket_t *oldBuckets = buckets();
bucket_t *newBuckets = allocateBuckets(newCapacity);
assert(newCapacity > 0);
assert((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);
//设置新表,设置mask值
setBucketsAndMask(newBuckets, newCapacity - 1);
if (freeOld) {
cache_collect_free(oldBuckets, oldCapacity);
cache_collect(false);
}
}
void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask)
{
mega_barrier();
_buckets = newBuckets;
// ensure other threads see new buckets before new mask
mega_barrier();
_mask = newMask;
_occupied = 0;
}
复制代码
因为是个mask
就只有那么大,如果容量变大了,那么mask
的值也会变话,那样再拿key
与新的mask
进行&
运算得出的结果肯定就不是原先的值了
因为key
值不确定,进行&
运算后很可能得到相同的值,这也是我们常说的hash冲突
,苹果是以cache_next
的方式去解决冲突(arm64)
static inline mask_t cache_next(mask_t i, mask_t mask) {
return i ? i-1 : mask;
}
复制代码
从当前位子一直向下找,找到了空位置,就填充进去,如果找不到,就从后面向前遍历查找
mask
,一直找到(i = cache_next(i, m)) != begin
总结
1,当方法调用的时候首先回去缓存中查找,找不到会遍历方法列表,在找不到会查找父类方法缓存,然后查找父类方法列表,一直找到rootClass
,找到后会加入本身类的方法缓存中,如果一直找不到就进行消息转发;
2,方法缓存是使用cache_t
缓存,它是一个散列表,当缓存空间大于3/4
时就需要扩容,并将先前存储的所有方法清空;
3,当第二次调用该方法时会在objc_msgSend
中查找一遍然后调用,所以可以打断点尝试下第二次调用的时候就不会在调用lookUpImpOrForward
了