OC 底层探索 11、objc_msgSend 流程 3 - 动态方法决议&消息转发
我们已经知道 objc_msgSend 的消息查找流程首先是 缓存 cache 查找,然后是去方法列表递归查找,若一直没有找到消息一般则会 crash 报错找不到该消息。
但是直接crash太过不友好,下面就进行探究苹果给我们的3次机会。
消息处理的流程图:
一、动态方法决议
1、通过简单代码切入
简单运行下面代码
运行崩溃:
在 MyPerson.m 中添加 resolveInstanceMethod: 方法:
再次run:发现一个问题1,为什么这里调用了2遍 resolveInstanceMethod() 呢?
走到了 resolveInstanceMethod 中,我们在这里对找不到的方法进行处理,然后再次run:
关于 v@: 方法签名具体可参考 OC 底层探索 05 。
上面代码运行后,不再崩溃,我们手动给 helloObj7 补充添加了 imp.
问题2:这里处理后,为何 resolveInstanceMethod() 方法又只走了一次呢?
针对这个 resolveInstanceMethod() 调用次数的问题文章后面继续进行探究。
2、resolveInstanceMethod 源码分析
方法入口
1. lookUpImpOrForward():
这里的判断条件在 resolveInstanceMethod() 过程中,只会进入一次,原因参见下图中注释:
2. resolveMethod_locked():
--> resolveInstanceMethod() / resolveClassMethod()
上面代码中可以看到,在这里 resolveXXXMethod 后会再次执行 return loopUpImpAndForward(),即:
--> 苹果给了一次机会 - 动态方法决议 - 无论是否处理都再查找一次 imp。
3. resolveInstanceMethod():
--> lookUpImpOrNil() --> lookUpImpOrForward()
1 /*********************************************************************** 2 * resolveInstanceMethod 3 * Call +resolveInstanceMethod, looking for a method to be added to class cls. 4 * cls may be a metaclass or a non-meta class. 5 * Does not check if the method already exists. 6 **********************************************************************/ 7 static void resolveInstanceMethod(id inst, SEL sel, Class cls) 8 { 9 runtimeLock.assertUnlocked(); 10 ASSERT(cls->isRealized()); 11 SEL resolve_sel = @selector(resolveInstanceMethod:); 12 13 // resolve_sel --> lookUpImpOrNil() --> lookUpImpOrForward() 14 if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) { 15 // Resolver not implemented. 16 // 查询没有 resolve_sel 这个 sel ,直接返回 17 return; 18 } 19 20 BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend; 21 bool resolved = msg(cls, resolve_sel, sel);// 发送消息,resolveInstanceMethod 22 23 // Cache the result (good or bad) so the resolver doesn't fire next time. 24 // +resolveInstanceMethod adds to self a.k.a. cls 25 IMP imp = lookUpImpOrNil(inst, sel, cls);// 再次去 lookUpImpOrForward 查询 imp,此imp是resolve补的 26 // lookUpImpOrNil -->lookUpImpOrForward(,,0|4|8=12) 27 28 if (resolved && PrintResolving) { 29 if (imp) { 30 _objc_inform("RESOLVE: method %c[%s %s] " 31 "dynamically resolved to %p", 32 cls->isMetaClass() ? '+' : '-', 33 cls->nameForLogging(), sel_getName(sel), imp); 34 } 35 else { 36 // Method resolver didn't add anything? 37 _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES" 38 ", but no new implementation of %c[%s %s] was found", 39 cls->nameForLogging(), sel_getName(sel), 40 cls->isMetaClass() ? '+' : '-', 41 cls->nameForLogging(), sel_getName(sel)); 42 } 43 } 44 }
从上面源码,我们可以得知,动态决议所走流程是一个循环套:
但是上面的 resolveInstanceMethod() 执行次数的问题我们还没有找到原因?下面进行细究。
查看打印时的堆栈信息:
resolveInstanceMethod() 调2次的原因探究
第一次打印:
第二次打印:
通过堆栈信息,我们可看到,
第一次打印流程: objc_msgSend_uncached --> lookUpImpOrForward --> resolveInstanceMethod().
第二次流程:CoreFoundation: forwarding --> CF: -[NSObject(NSObject) methodSignatureForSelector:] --> __methodDescriptionForSelector
--> class_getInstanceMethod() --> lookUpImpOrForward --> resolveInstanceMethod().
通过查看堆栈信息我们可以知道 resolveInstanceMethod() 第二次是因系统的消息签名机制后被调起的,CoreFoundation 中做了什么呢?文章底部对其进行分析。
tip:关于类方法动态方法决议有个点要注意下,我们直接把 resolveClassMethod() 给到当前类进行处理是不会生效的,这里涉及到isa走位和继承链关系,类方法是元类的实例方法根元类的父类是NSObject。详见:OC 底层探索 04.
动态方法决议的存在有什么意义呢?
个人想法
1、这里可以做一些埋点、问题统计等优化处理;
2、我们或许可以用其进行 封装 切面?例如:封装SDK,定义的方法全部以 XXX_ 为前缀,针对未实现方法崩溃的问题,进行处理并记录上报问题点。但是,我们知道方法调用优先级是 子>父,如果出现在局部子类对 resolve 方法做了处理,那么封装其实便不会走,也浪费封装,所以在此处进行封装是不合适的。所以,这些类似操作应该在更具有可操作性的场景方法中使用,一般在 resolveInstanceMethod 这里不作处理。留下个小问题:什么场景适于使用切面呢?如何使用呢?
二、消息转发
动态方法决议的机会不作处理,此后会走到消息转发的流程,以下代码均以 MyPerson.m 为示例操作。
1、快速转发
forwardingTargetForSelector:
- (id)forwardingTargetForSelector:(SEL)aSelector { NSLog(@"快速转发这里需要 sel == %@",NSStringFromSelector(aSelector)); // return [super forwardingTargetForSelector:aSelector]; return [MyChild alloc];// 消息转给 MyChild }
在此方法中我们只需给 SEL - aSelector 一个 imp 即可。
2、签名转发
methodSignatureForSelector:
forwardInvocation:
///消息签名 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { NSLog(@"需要消息签名 sel == %@",NSStringFromSelector(aSelector)); // return [super methodSignatureForSelector:aSelector]; // 给一个方法签名 // v@: --> 返回类型void,参数类型id,SEL return [NSMethodSignature signatureWithObjCTypes:"v@:"]; } - (void)forwardInvocation:(NSInvocation *)anInvocation {// Invocation 启用 调用 NSLog(@"签名过来了"); }
如上代码,运行程序:
当前 invocation 中包含了什么?
我们签名中 return 了一个方法签名,invocation 是这个抛出来事务,但我们并未对 “helloObj7” 方法进行处理,它目前仍是没有imp的,系统为什么会不再崩溃呢?
消息签名的慢速转发机制相当于,系统允许,对于此签名的事务,可以处理,也可不处理。相当于抛出去就不必管它了,任其随意游离在哪里;我这里不处理了,爱谁处理谁处理。像飘在空中的云 大家都可以看见,同时也可以不看。
但是这里不处理,我们就浪费掉了一个事务,你调用到了它,响应却不处理没利用它,耗费了性能,同样也是业务层面的一种浪费。
! 慢速转发相对快速转发更灵活,权限更大。
对 invocation 进行处理,让它去处理任一方法(我们可以根据实际业务需求对此方法做成统一处理某件事):
注意一点:
如果在这里把“helloObjc7”这个sel 给到invocation,然后对invocation启动后,会造成无线循环调用,一直去找这个不存在的“helloObjc7” --> crash.
三、信息找寻 - 流程验证
我们回到代码未做任何处理的初始状态,运行:
通过堆栈信息查看为什么会报错,我们可以发现在main函数后、报错前,还有2个方法,它们具体做了什么如何查看呢?它们都是属于 CoreFoundation框架的,我们去找foundation的源码:CF 相关源码 链接.
将源码拖到 Visual Studio Code 中 (VSCode 下载),搜索__forwarding_pre_0___,找不到!没有相应开源源码 --> 反汇编。
反汇编
Hopper 是一个可以将可执行文件反汇编成伪代码、控制流程图等,帮助我们可以进行文件可视性分析的反汇编工具。
1、通过 image list 读取全部加载的镜像文件,找到 CoreFoundation 的位置:
2、前往文件,找到 coreFoundation 可执行文件:
3、开启 hopper :
1)Try the Demo
2)将可执行文件拖入 hopper:
3)功能界面如下:
4、开始找寻我们需要的信息
1)搜索 __forwarding_prep_0___ --> 进入 ___forwarding___的伪代码
___forwarding___:
消息快速转发 forwardingTargetForSelector 没有实现怎跳转到 loc_6459b位置,走到消息签名方法:
1.1)消息签名 methodSignatureForSelector 未实现,跳转 loc_6490b:报错
1.2)消息签名 methodSignatureForSelector 实现了则继续往下走:
判断 forwardingInvocation 响应则继续向下走(loc_6476f):
invocation 不响应则报错:
从上面流程也验证了我们顶部的消息流程图。
2)如法炮制,搜索 __methodDescriptionForSelector
--> class_getInstanceMethod:
class_getInstanceMethod 源码如下,我们可以看到内部调用了 lookUpImpOrForwarding:
这里,可验证为何上面 resolveInstanceMethod() 调用了2次。