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 |
|
从上面源码,我们可以得知,动态决议所走流程是一个循环套:
但是上面的 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:
1 |
|
在此方法中我们只需给 SEL - aSelector 一个 imp 即可。
2、签名转发
methodSignatureForSelector:
forwardInvocation:
1 |
|
如上代码,运行程序:
当前 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 次。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!