OC底层原理14-3:objc_msgSend之动态方法决议 & 消息转发
前言
在前面两篇文章中,分别分析了
objc_msgSend
的快速查找(缓存)
和慢速查找(方法列表)
,其整个流程:objc_msgSend -> 慢速缓存查找(没有) -> 慢速方法列表二分查找自己(有) -> cache_fill -> objc_msgSend 整个闭环
慢速方法列表二分查找没有找到 -> 父类去查找(缓存 -> 方法列表) -> 根类(NSObject)也没找到
在这两种方法都没找到方法实现的情况下,苹果给了两条建议
动态方法决议
:慢速查找流程未能找到,会执行一次动态方法决议消息转发
:如果动态方法决议仍然没有找到实现,则进行消息转发
如果这两个建议都没有作出任何操作,就会报我们日常开发中常见的
方法未实现
的崩溃报错
,其步骤如下定义
ZJPerson
类,其中say666
实例方法 和sayNB
类方法均没有实现
- 调用类方法
sayNB
的报错结果
方法未实现报错源码
根据 慢速查找
的源码,我们发现,其报错最后都是走到 __objc_msgForward_impcache
方法,本质是调用的 objc_defaultForwardHandler
方法
下面我们来讲讲如何在崩溃前,防止方法未实现的崩溃
三次方法查找的挽救机会
根据苹果建议,我们一共有三次挽救的机会:
【第一次机会】:
动态方法决议
消息转发流程:
- 【第二次机会】:
快速转发
- 【第三次机会】:
慢速转发
- 【第二次机会】:
【第一次机会】动态方法决议
在 慢速查找
流程 未找到
方法实现时,首先会 尝试一次动态方法决议
,其源码实现如下:
1 | static NEVER_INLINE IMP |
主要分为以下几个步骤
判断类是否是元类
- 如果是
类
,执行实例方法
的动态方法决议resolveInstanceMethod
- 如果是
元类
,执行类方法
的动态方法决议resolveClassMethod
,如果在元类中没有找到
或者为空
,则在元类
的实例方法
的动态方法决议resolveInstanceMethod
中查找,主要是因为类方法在元类中是实例方法
,所以还需要查找元类中实例方法的动态方法决议
- 如果是
如果
动态方法决议
中,将其实现指向了其他方法
,则继续查找指定的imp
,即继续慢速查找lookupImpOrForward
实例方法
针对 实例方法
调用,在 快速、慢速查找
均没有找到 实例方法
的实现时,我们有一次挽救的机会,即尝试一次 动态方法决议
,由于是 实例方法
,所以会走到 resolveInstanceMethod
方法,其源码如下:
1 | static void resolveInstanceMethod(id inst, SEL sel, Class cls) |
主要分为以下几个步骤
在
发送resolveInstanceMethod消息
前,需要查找cls类
中是否有该方法的实现
,即通过lookupImpOrNil
方法又会进入lookupImpOrForward
慢速查找流程查找resolveInstanceMethod
方法- 如果没有,则直接返回
- 如果有,则发送
resolveInstanceMethod
消息
再次慢速查找实例方法的实现,即通过
lookupImpOrNil
方法又会进入lookupImpOrForward
慢速查找流程查找实例方法
崩溃修改
所以,针对 实例方法sya666
未实现的报错崩溃,可以通过在 类
中 重写resolveInstanceMethod类方法
,并将其指向其他方法的实现,即在 ZJPerson
中 重写resolveInstanceMethod类方法
,将 实例方法sya666
的实现指向 sayMaster
方法实现,如下所示
1 | @interface ZJPerson : NSObject |
重新运行,其打印结果如下:
类方法
针对 类方法
,与实例方法类似,同样可以通过重写 resolveClassMethod
类方法来解决前文的崩溃问题,即在 ZJPerson
类中重写该方法,并将 sayNB
类方法的实现 指向类方法zjClassMethod
1 | @interface ZJPerson : NSObject |
resolveClassMethod
类方法的重写需要注意一点,传入的 cls
不再是类,而是 元类
,可以通过 objc_getMetaClass
方法 获取元类
,原因是因为 类方法在元类中是实例方法
优化
上面的这种方式是单独再每个类中重写,有没有更好的,一劳永逸的方法呢?其实通过方法慢速查找流程可以发现其查找路径有两条
- 实例方法:
类 -> 父类 -> 根类 -> nil
- 类方法:
元类 -> 根元类 -> 根类 -> nil
它们的共同点是如果前面没找到,都会来到 根类即NSObject中查找
,所以我们是否可以将上述的两个方法统一整合在一起呢?答案是 可以的
,可以通过 NSObject添加分类
的方式来 实现同意处理,而且由于类方法的查找,在其集成链,查找的也是实例方法,所以将 实例方法
和 类方法
的统一处理放在 resolveInstanceMethod
方法中,如下所示:
1 | @interface NSObject (ZJ) |
这样方式的实现,正好与源码中针对类方法的处理逻辑是一致的,即完美阐述为什么调用了类方法动态方法决议,还要调用对象方法的动态方法决议,其根本原因还是 类方法在元类中的实例方法
当然,上面这种写法还是会有其他问题,比如 系统方法也会被更改
,针对这一点,是可以优化的,即我们 可以针对自定义类中方法同意方法名的前缀
,根据前缀来判断是否是自定义方法,然后 统一处理自定义方法
,例如可以在崩溃前pop到首页,主要是用于 app线上防崩溃的处理
,提升用户的体验
消息转发流程
在慢速查找的流程中,我们了解到,如果快速+慢速没有找到方法实现,动态方法决议也不行,就使用 消息转发
,但是,我们找遍了源码也没有发现消息转发的相关源码,可以通过以下方式来了解,方法调用崩溃前都走了哪些方法
- 通过
instrumentObjcMessageSends
方式打印发小消息的日志 - 通过
hopper/IDA反编译
通过 instrumentObjcMessageSends
通过
lookUpImpOrForward -> log_and_fill_cache -> logMessageSend
,在logMessageSend源码下方找到instrumentObjcMessageSends
的源码实现,在main中调用instrumentObjcMessageSends
打印方法调用的日志信息,有以下两点准备工作- 打开
objcMsgLogEnabled
开关,即调用instrumentObjcMessageSends
方法时,传入YES
- 在
main
中通过extern
声明instrumentObjcMessageSends
方法
- 打开
1 | extern void instrumentObjcMessageSends(BOOL flag); |
- 通过
logMessageSend
源码,了解到消息发送打印信息存储在/tmp/msgSends
目录,如下所示:
运行代码,并前往
/tmp/msgSends/
目录,发现有msgSends
开头的日志文件,打开发现崩溃前,执行了以下方法:- 两次
动态方法决议
:resolveInstanceMethod
方法 - 两次
消息快速转发
:forwardingTargetForSelector
方法 - 两次
消息慢速转发
:methodSignatureForSelector + resolveInvocation
- 两次
通过hopper/IDA反编译
Hopper
和 IDA
是一个可以帮助我们静态分析可视性文件的工具,可以将执行文件反编译成伪代码、控制流程图等,下面以Hopper为例(注:hopper高级版本是一款收费软件,针对比较简单的反汇编需求来说,demo版本足够使用了)
- 运行程序崩溃,查看堆栈信息
发现
__forwarding__
来自CoreFoundation
通过
image list
,读取整个镜像文件,然后搜索CoreFoundation
,查看其可执行文件的路径
- 通过文件路径,找到
CoreFoundation
的可执行文件
- 打开hopper,选择
Try the Demo
,然后将上一步的可执行文件拖入hopper进行反汇编,选择x86(64 bits)
- 以下是反汇编后的界面,主要使用上面的三个功能,分别是 汇编、流程图、伪代码
通过左侧的搜索框搜索
__forwarding_prep_0___
,然后选择伪代码- 以下是
__forwarding_prep_0___
的汇编伪代码,跳转至___forwarding___
- 以下是
以下是___forwarding___的伪代码实现,首先是查看是否实现forwardingTargetForSelector方法,如果没有响应,跳转至loc_6459b即快速转发没有响应,进入慢速转发流程
- 跳转至loc_6459b,在其下方判断是否响应methodSignatureForSelector方法
如果没有响应,跳转至loc_6490b,则直接报错
如果获取methodSignatureForSelector的方法签名为nil,也是直接报错
如果methodSignatureForSelector返回值不为空,则在forwardInvocation方法中对invocation进行处理
所以,通过上面两种查找方式可以验证,消息转发
的方法有3个
- 【快速转发】
forwardingTargetForSelector
- 【慢速转发】
methodSignatureForSelector
forwardInvocation
所以,综上所述,消息转发整体的流程如下
消息转发
的处理主要分为两部分:
【快速转发】当慢速查找以及动态方法决议没有找到实现时,进行消息转发,首先是进行
快速消息转发
,即走到forwardingTargetForSelector
方法如果返回
消息接收者
,在消息接收者中还是没有找到,则进入另一个方法的查找流程如果返回
nil
,则进入慢速消息转发
【慢速转发】执行到
methodSignalForSelector
方法如果返回的
方法签名
为nil
,则直接崩溃报错
如果返回的方法签名
不为nil
,走到forwardInvocation
方法中,对invocation事务进行处理,如果不处理也不会报错
【第二次机会】快速转发
针对前文的崩溃问题,如果 动态方法决议
也没有找到方法实现,则需要在 ZJPerson
中重写 forwardingTargetForSelector
方法,将ZJPerson的实例方法的 接收者指定为ZJStudent
的对象(ZJStudent类中有say666的具体实现),如下所示
1 | // ZJPerson类 |
执行结果如下
也可以直接不指定消息接收者,直接调用父类的该方法
,如果还是没有找到,则 直接报错
【第三次机会】慢速转发
针对 第二次机会即快速转发
中还是没有找到,则进入最后的一次挽救机会,即在 ZJPerson
中重写 methodSignalForSelector
,如下所示
1 | // 慢速转发 |
打印结果如下,发现 forwardInvocation
方法中不对 invocation
进行处理,也不会崩溃
也可以 处理invocation
事务,如下所示,修改 invocation
的 target
为 [ZJStudent alloc]
,调用 [anInvocation invoke]
触发 即ZJPerson
类的 say666
实例方法的调用会调用 ZJStudent
的 say666
方法
1 | - (void)forwardInvocation:(NSInvocation *)anInvocation{ |
执行结果如下
所以,由上述可知,无论在 forwardInvocation
方法中 是否处理invocation
事务,程序都不会崩溃
总结
到目前为止,objc_msgSend 发送消息的流程就分析完成了,在这里简单总结下
【快速查找流程】在类的
缓存cache
中查找指定方法的实现【慢速查找流程】如果缓存中
没有找到
,则在类的方法列表
中查找,如果还是没有找到,则去父类的缓存和方法列表中
查找【动态方法决议】如果慢速查找
还是没有找到
,第一次挽救的机会就是尝试一次动态方法决议
,即重写resolveInstanceMethod/resolveClassMethod
方法【消息转发】如果动态方法决议还是没有找到,则进行
消息转发
,消息转发中有两次挽救几会:快速转发+慢速转发
如果转发之后也没有,则程序直接报错崩溃
unrecognized selector sent to instance
- Post title:OC底层原理14-3:objc_msgSend之动态方法决议 & 消息转发
- Post author:张建
- Create time:2020-10-06 10:13:46
- Post link:https://redefine.ohevan.com/2020/10/06/OC底层原理/OC底层原理14-3:objc_msgSend之动态方法决议&消息转发/
- Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.