梳理下
调用方法的流程,避免大家迷路。
- 我们
对象(实例对象或类)调用方法,都是执行objc_msgSend:- step1: 进入
汇编语言,在cache中快速查找,找到了返回imp,没找到走step2- step2: 进入
c/c++底层,在methodList中查找,(会将方法写入缓存,保障后续调用时,能直接在第一步就获取到imp),找到了的返回imp,没找到走step3- step3: 走
最后的处理机制(三重防护,这个后面详细介绍),没找到走step4- step4: 执行默认的
imp,报错提示,crash。
上一节已了解方法在cache中的高速查找流程,当cache中找不到imp时,我们进入step2:
方法慢速查找流程
请跟着我,一步步细致理解。我相信你会收货满满的干货
- 了解高速
cache转低速c/c++底层过程 - 初始值forward_imp
- 慢速前的cache读取
- 检查类是否合法
- 类的初始化(双向链表)
- 当前类和父类循环查找imp
- 最后的防线
- 查找结果
1. 了解高速cache转低速c/c++底层过程
我们选择
arm64真机环境,在objc4源码中进行探索。上一节我们在
缓存中找不到imp后,就来到了__objc_msgSend_uncached->MethodTableLookup方法列表查找 ->_lookUpImpOrForward。objc4源码中搜索_lookUpImpOrForward,发现没有具体实现。 取消_,搜索lookUpImpOrForward:

- 发现
lookUpImpOrForward不再是汇编语言,而是oc底层语言。这也是为什么说从这开始,就是从快速搜索(汇编)回到慢速搜索(c/c++)
代码检验流程
main.m中加入测试代码。 在[p sayHello];加入断点@interface HTPerson : NSObject - (void)sayHello; @end @implementation HTPerson - (void)sayHello{ NSLog(@"%s",__func__); } @end int main(int argc, const char * argv[]) { @autoreleasepool { HTPerson *p = [HTPerson alloc]; [p sayHello]; } return 0; }代码检验.jpg
-
lookUpImpOrForward的代码在OC底层原理十: 类的深入理解中有讲到,这里我们做更细致的分析。
2. 初始值forward_imp
lookUpImpOrForward函数第一步,设置初始变量(forward_imp、imp、curClass)
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
IMP imp = nil;
Class curClass;
我们先弄清楚forward_imp是什么?
- 全局搜索
_objc_msgForward_impcache

注:
1、C/C++调用汇编,去汇编中查找时: 在方法前多加一个下划线
2、汇编调用C/C++方法,去C/C++中查找时: 在方法前减一个下划线
objc_defaultForwardHandler(id self, SEL sel)
{
_objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
"(no message forward handler is installed)",
class_isMetaClass(object_getClass(self)) ? '+' : '-',
object_getClassName(self), sel_getName(sel), self);
}
结论: forward_imp 的值就是未找到imp的函数实现
3. 慢速前的cache读取
在进入lookUpImpOrForward函数前,我们在汇编MethodTableLookup宏中了解behavior的值为3

lookUpImpOrForward函数继续往下看:
if (fastpath(behavior & LOOKUP_CACHE)) {
imp = cache_getImp(cls, sel);
if (imp) goto done_nolock;
}
因为LOOKUP_CACHE = 4, behavior = 3,二进制得出 behavior & LOOKUP_CACHE为0。
-
if判断条件不成立。
初次进入lookUpImpOrForward是从缓存进入的,所以没必要再在缓存中进行查询,但是后续再次进入时,控制入参behavior的值,会触发cache_getImp缓存查询
我们看看cache_getImp,发现又回到了高速的汇编层,全局搜索_cache_getImp:

发现调用了CacheLookup,继续在cls的cache中高速查找当前sel对应的imp。
CacheLookup高速查找流程在上一节已详细介绍
主要目的:
可能
多个线程都在执行objc_msgSend任务,在你第一次高速查找未找到时,可能其他线程已将cls的sel和imp写入了cls的缓存。高速查找(汇编)比低速查找(c/c++)性能快太多了,我们进入慢速查找前,我们再走一次高速缓存查找。 (缓存读取不到再过来lookUpImpOrForward的,默认走了一遍高速缓存了,就没再进入了)如果获取到
imp,就跳转到done_nolock(下面一起分析), 如果没有,进入慢速查找流程。
4. 检查类是否合法
checkIsKnownClass(cls);
确保cls在已知类列表中,避免传入一个非类的二进制文件,进行CFI攻击。
5. 类的初始化(双向链表)
确保cls已存在,将cls赋值给curClass
- 如果
不存在,就现在创建类( 因为swift和OC类的结构不一样,所以需要单独判断是否存在。)
// 如果类没有实现或内部信息不完整
if (slowpath(!cls->isRealized())) {
// 实现类(swift和oc) 递归实现完整的继承链和isa指向链
cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
}
// 如果类没有初始化
if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
// 完成类的初始化
cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
}
// 此时运行时锁一定是锁定状态
runtimeLock.assertLocked();
// 拿到了cls类 (oc类或swift类)
curClass = cls;
5.1 类的实现(完善继承链和isa链,双向绑定)

此处目的是cls的所有内容赋值,完成继承链和isa指向链的完整绑定。便于后续沿着继承链搜索sel对应的imp。
5.2 类的初始化
检查本类及继承链上的类是否都初始化,确保后续可以对类进行操作。

6.当前类和父类循环查找imp
// unreasonableClassCountt为 353056。一个足够大的值,只要比继承链类的个数大,就循环有效)
for (unsigned attempts = unreasonableClassCount();;) {
// 在curClass的函数列表中搜索sel方法
Method meth = getMethodNoSuper_nolock(curClass, sel);
// 如果存在,将imp赋值为sel对应的imp。跳到下面done继续操作
if (meth) {
imp = meth->imp;
goto done;
}
// 如果没有找到, curClass赋值为curClass的父类,判断父类是否存在
if (slowpath((curClass = curClass->superclass) == nil)) {
// imp取初始值forward_imp(错误提示),跳出for操作。
imp = forward_imp;
break;
}
// 防止无限循环
if (slowpath(--attempts == 0)) {
_objc_fatal("Memory corruption in class list.");
}
// 【进入循环】
// cache_getImp走汇编,循环从父类缓存中高速寻找sel对应的Imp
imp = cache_getImp(curClass, sel);
// 如果imp是初始值forward_imp(错误提示),就跳出循环
if (slowpath(imp == forward_imp)) {
break;
}
// 在父类中找到imp后,跳到下面done继续操作
if (fastpath(imp)) {
goto done;
}
}

7.最后的防线
我们看到,上一步操作中,如果没找到imp,都会break跳出for操作,进入下面的动态尝试。
- 这是
慢速查找最后的倔强,也是系统给予sel方法的最后一次补救机会。
// behavior 默认为3, LOOKUP_RESOLVER为2
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER; // behavior取LOOKUP_RESOLVER的反
return resolveMethod_locked(inst, sel, cls, behavior);
}
这段代码
只执行一次
- 首次进入:
behavior=3,LOOKUP_RESOLVER= 2,behavior & LOOKUP_RESOLVER= 2 (二进制计算), if条件成立。
behavior取LOOKUP_RESOLVER的反,下一次behavior和LOOKUP_RESOLVER进行与运算时,结果一定为0。
(比如001取反是110, 他们每个位都相反,所以与运算的结果为000)所以这个补救机会,只会进入一次。
- 这一步包括
动态方法决议等三步挽救法,我们在下一节详细讲解。
本节我们只需要知道,如果最后挽救失败,程序就会抛出forward_imp异常内容。并crash了。
8.查找结果
当我们找到
imp时,就会直接进入done流程,将cls的sel和imp写入缓存中(便于下次同样的方法,可以在cache汇编快速查询到)。done结束后,我们会进入done_nolock流程, 如果imp是forward_imp,就返回nil,否则,返回正常的imp。
done:
// 从将sel和imp写入cls的缓存
log_and_fill_cache(cls, imp, sel, inst, curClass);
// 运行时解锁
runtimeLock.unlock();
done_nolock:
//如果不需要找了(LOOKUP_NIL),并且imp等于forward_imp,就返回nil。
if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
return nil;
}
// 返回当前获取的imp
return imp;
下一节,OC底层原理十四: objc_msgSend (消息转发 + 汇总图),我们会探究最后的防线,并梳理objc_msgSend的完整流程 。
