简介
ObjC Runtime 的消息传递过程中,会使用方法缓存提高效率。
本文主要是记录方法缓存的几个特点:
- 每个类有一个方法缓存,而不是每个对象都有一个缓存。
- 方法缓存是一个散列表。
- 若调用的是父类的方法,也会添加到本类的缓存里。
消息传递
先简单回顾下。
id objc_msgSend(id self, SEL _cmd, ...);
,即消息传递的过程:
- 判断 receiver,若为 nil,则 return,否则 2
- 在方法缓存中寻找,找到则返回 imp,否则 3
- 在本类「方法列表」中寻找,若找不到就在父类中查找,溯源而上,找到后添加到缓存中,并返回 imp,否则触发转发机制。
id _objc_msgForward(id self, SEL _cmd,...);
,消息转发的过程:
- 询问本类能不能实现处理这种情况?
- (在本类中询问)有没有「备胎」能处理这种情况?
- Runtime System 会创建一个 NSInvocaton 对象,保存相关信息,然后全局通知,看谁能处理它。
方法缓存存在类中
缓存的声明集中在 objc-runtime-new.h
中:
// objc-runtime-new.h
struct objc_class : objc_object {
...
cache_t cache;
...
}
从源码中可以看到缓存是保存在 class 中,而不是实例中。
方法缓存是一个散列表
从缓存中查找方法的源码主要在 objc-cache.mm
中
// objc-runtime-new.h
struct cache_t {
// 别误以为是数组,接下来的 find 函数,可以清楚知道它是作为散列表来使用的
struct bucket_t *_buckets;
...
struct bucket_t * find(cache_key_t key, id receiver);
}
// objc-cache.mm
bucket_t * cache_t::find(cache_key_t k, id receiver)
{
assert(k != 0);
bucket_t *b = buckets();
mask_t m = mask();
// note by jd: 根据 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);
}
从源码中可知,方法缓存是一个散列表。
那为什么方法列表是一个数组,而不使用散列表呢?
个人认为有以下原因:
- 散列表,哈希计算会有消耗,而且会有空位。这会浪费空间的。
- 尤其是在 Category 插入方法时,若出现冲突,将影响加载效率。
父类方法也会添加到本类缓存中
当从缓存中找不到方法,会调用以下函数。
// objc-runtime-new.mm
/***********************************************************************
* _class_lookupMethodAndLoadCache.
* Method lookup for dispatchers ONLY. OTHER CODE SHOULD USE lookUpImp().
* This lookup avoids optimistic cache scan because the dispatcher
* already tried that.
**********************************************************************/
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
...
// Try superclass caches and method lists.
{
unsigned attempts = unreasonableClassCount();
for (Class curClass = cls->superclass;
curClass != nil;
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 {
break;
}
}
// Superclass method list.
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
}
...
}
从源码中可以看到,会先后从父类缓存、方法列表中查找,找到后则保存在自己的方法缓存中。
小结
ObjC Runtime 使用方法缓存,以提高消息传递的效率。
它被设计成散列表的形式,保存在每个类中。
另外,即便是父类的方法,也会被缓存在本类中。
网友评论