前言:我们前面通过方法的本质,了解到了objc_msgSend函数的一个快速缓存查找过程,如果在进行快速查找,寻找不到IMP的情况下,查找过程并不会结束,而是会开始另外一个过程————慢速查找过程。那么接下来就让我们通过一系列的分析,来了解objc_msgSend的慢速查找过程!
runtime消息发送系列文章友情链接
一、objc_msgSend过程回顾
我们在进行消息发送的时候,会调用objc_msgSend(receiver,_cmd)方法进行消息的发送,其中receiver是消息的接受者,_cmd是消息的内容。如下:
Person *p = [Person alloc];
[p test];
//转化为cpp
Person *p = ((Person *objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));
objc_msgSend(p, sel_registerName("test"));
1、objc_msgSend快速查找回顾
1、通过类找到isa
2、拿到isa通过偏移量找到cache_t
3、然后拿到存储方法的buckets,尝试通过哈希数组拿到bucket中的方法IMP
4、如果获取到就返回,如果找不到就继续遍历其他的bucket
5、最后进入慢速查找过程。
2、objc_msgSend慢速查找
这里,我们要从一个函数说起 ———— lookUpImpOrForward,快速查找的汇编分析中,我们跟踪到这个函数后,就开始了消息查找的另一段旅程 ———— 慢速查找过程!
二、objc_msgSend慢速查找解析
1、lookUpImpOrForward方法
- 以下程序是runtime中lookUpImpOrForward方法的实现,具体流程请参照注释:
#pragma mark - 消息查找流程,核心内容
NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
#pragma mark - 从_objc_msgForward_impcache中寻找IMP
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
IMP imp = nil;
Class curClass;
runtimeLock.assertUnlocked();
#pragma mark - 在第一次初始化,做一下类还未Initialized的标记
if (slowpath(!cls->isInitialized())) {
behavior |= LOOKUP_NOCACHE;
}
runtimeLock.lock();
#pragma mark - 检查类是否进行过Initialized绑定操作
checkIsKnownClass(cls);
cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
runtimeLock.assertLocked();
curClass = cls;
#pragma mark - 通过for循环,找出当前类的方法
for (unsigned attempts = unreasonableClassCount();;) {
if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
#pragma mark - ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
#pragma mark - 开始寻找缓存和方法列表中的方法
imp = cache_getImp(curClass, sel);
if (imp) goto done_unlock;
curClass = curClass->cache.preoptFallbackClass();
#endif
} else {
#pragma mark - 寻找当前类中的方法列表中的方法(二分查找法寻找方法)
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
imp = meth->imp(false);
goto done;
}
#pragma mark - 未找到方法并且未进行后续处理,跳出循环
if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
imp = forward_imp;
break;
}
}
#pragma mark - 父类的二分查找尝试次数结束,发送错误通知
if (slowpath(--attempts == 0)) {
_objc_fatal("Memory corruption in class list.");
}
#pragma mark - ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
#pragma mark - 本类没有找到方法,开始寻找父类的缓存和方法列表中的方法
imp = cache_getImp(curClass, sel);
if (slowpath(imp == forward_imp)) {
#pragma mark - 发现缓存中存在imp,跳出当前循环,去处理imp
break;
}
if (fastpath(imp)) {
#pragma mark - 发现imp在父类中,将其缓存到自己的类信息中,然后处理imp
goto done;
}
}
#pragma mark - 如果imp没有实现,则进入消息动态解析过程,如果实现,就不处理,继续进行后续
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
#pragma mark - ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
#pragma mark - ************** 核心事务处理过程 ************
#pragma mark - ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
/*
* done内做的工作:
* 1、如果cache中存在IMP,就从cache中取出IMP返回
* 2、打印并且插入方法到当前的类信息中
* 3、如果没找到缓存或者未找到imp相同的方法,则返回nil
*/
done:
if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES
while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
cls = cls->cache.preoptFallbackClass();
}
#endif
log_and_fill_cache(cls, imp, sel, inst, curClass);
}
done_unlock:
runtimeLock.unlock();
if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
return nil;
}
return imp;
}
2、消息慢速查找流程图
3、查找过程的关键点
快速查找过程我们分析了汇编相关的实现,那么慢速查找中也是有相关的难点存在的。比如查找方法的过程中,是根据什么进行上层查找的,查找的算法实现等。那么我们接下来就根据这些关键的代码实现进行分析!
三、查找过程中的关键点分析
3.1、在类中怎么寻找缓存中的方法?
meth = _cache_getMethod(curClass, sel, _objc_msgForward_impcache);
- 从查找过程的函数分析,关键过程在_cache_getMethod的实现,在runtime的c++代码中查找不到实现,于是,我们搜索汇编的实现:
#pragma mark 中间会经历isa->class->cache_t->buckets->bucket->SEL IMP的运算过程
#pragma mark 中间会经历isa->class->cache_t->buckets->bucket->SEL IMP的运算过程
#pragma mark 中间会经历isa->class->cache_t->buckets->bucket->SEL IMP的运算过程
1: cmp p0, #0
2: GetClassFromIsa_p16 p13, 1, x0 // p16 = class
3: CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
3.1 : LLookupStart -> CACHE_MASK_STORAGE_HIGH_16
// 获取cache
ldr p11, [x16, #CACHE] // p11 = mask|buckets
CONFIG_USE_PREOPT_CACHES == 1
// p11 的 0 号位置是否为0 不为0 -> LLookupPreopt
tbnz p11, #0, LLookupPreopt\Function
and p10, p11, #0x0000ffffffffffff // p10 = buckets
// p1 sel >> 7 == value ^= value >> 7;
eor p12, p1, p1, LSR #7
// 哈希 index
and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask
// index << 4
// 2 * 16
// buckets + 32
// 对应下标 : p13 第一个要查bucket(sel imp)
add p13, p10, p12, LSL #(1+PTRSHIFT)
// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
// sel -> imp
3.2、在类中怎么寻找类的方法列表中的方法的?
- 查找过程中,调用了以下方法进行了方法查找。
#pragma mark - 寻找当前类中的方法列表中的方法(二分查找法寻找方法)
Method meth = getMethodNoSuper_nolock(curClass, sel);
- getMethodNoSuper_nolock内部实现了cls->data()->methods()的过程。
static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
.......
auto const methods = cls->data()->methods();
for (auto mlists = methods.beginLists(),
end = methods.endLists();
mlists != end;
++mlists)
{
method_t *m = search_method_list_inline(*mlists, sel);
if (m) return m;
}
return nil;
}
四、整体思路总结:
1、先判断类是否存在,不存在,返回空,否则继续,
2、查找类自己的缓存(class's cache),找到IMP就返回,否则就继续,
3、查找类自己的方法列表(class's method lists),找到就返回IMP,否则继续,
4、查找父类的缓存(superclass)和方法列表(method lists),找到返回IMP,否则继续
5、如果找到IMP,且IMP不是本类的IMP,就将父类的IMP缓存拷贝到自己的cache下一份使用,
6、如果没有IMP,则执行动态方法决议和消息转发流程。
以上过程是对整个方法慢速查找过程的总结。