iOS 消息转发机制详解

243 阅读3分钟

前言

有以下的简单代码,声明了一个类 Doge,里面有一个实例方法run,但是并不实现run方法 iShot2021-12-26 19.57.11.pngmain函数里,创建一个Doge实例dog并调用run iShot2021-12-26 19.56.41.png 很明显,运行后是会崩溃的。

这也是一个日常开发中很常见的崩溃,清理代码的时候可能一不小心就误删了某个测试方法实现。

在声明还保留的情况下没运行到该方法是不会发生崩溃的,这时候就相当尴尬。

那么如何避免这种问题呢,首先想到的是创建一个测试类扩展,并在里面声明并实现测试方法 iShot2021-12-26 20.07.05.png

但是这样操作每次都要创建很多类扩展文件,清理起来也相当麻烦,那么是否有办法能一劳永逸呢?

动态方法解析 resolveInstanceMethod

熟悉方法调用的同学知道其实质是发送消息,也就耳熟能详的objc_msgSend

不熟悉的同学请看方法调用/消息发送详解

SELIMP又是什么关系呢,用书来比喻的话SEL相当于目录,页码相当于IMP(指向函数具体实现的指针),书页的具体内容就相当于函数实现。

为什么提到这个呢,因为我们是方法调用导致的崩溃,所以从方法调用流程入手找解决方案是最好的。

在慢速查找方法实现(也就是lookUpImpOrForward的过程)中,找不到方法(找不到IMP)时就会调用resolveMethod_locked方法。 iShot2021-12-26 21.58.02.png

进入resolveMethod_locked,在这里会判断传入的cls参数是否为元类 iShot2021-12-26 22.00.13.png

  • 如果是,则说明原本执行的方法为类方法
  • 如果否,则说明原本执行的方法为实例方法

两者的虽然分为了两个分支,但处理大同小异,进入resolveInstanceMethod,可以看到该方法会通过lookUpImpOrNilTryCache获取重新获取IMPiShot2021-12-26 22.20.29.png

resolveInstanceMethod流程图如下 resolveInstanceMethod.png

所以我们需要做的就是在出问题的类(或者其父类,甚至是NSObject)里面重写resolveInstanceMethod方法并并为缺失实现的方法添加IMP iShot2021-12-26 22.46.50.png

此时,在resolveInstanceMethod断点,可以看到能获取到新的IMP iShot2021-12-26 23.06.44.png

放开断点,成功解决崩溃 iShot2021-12-27 00.16.37.png

快速消息转发 forwardingTargetForSelector

resolveInstanceMethod没有处理成功的时候,对象就会调用forwardingTargetForSelector,通过返回别的对象处理该消息。

简单来说,就是我的Doge虽然有run这个功能,但是他年龄没到没法实现。我知道Cat也能run我就让Cat对象去run iShot2021-12-27 00.33.10.png

可以看到,也是成功处理了崩溃 iShot2021-12-27 00.36.14.png

标准消息转发 methodSignatureForSelector & forwardInvocation

如果消息无人认领,也就是forwardingTargetForSelector无法处理的时候,可以通过标准消息转发流程处理。

首先通过class_getInstanceMethodmethod_getTypeEncoding获取另一个已经实现的方法类型并用其创建一个NSMethodSignature

然后重写forwardInvocation并把上述获取签名的方法赋值到anInvocation.selector即可

iShot2021-12-27 00.54.22.png

总结

至此,消息转发流程的全流程已经结束。

上述的解决方案都在发生问题的类(Doge)中编写代码,如果想统一处理的话只需要创建NSObject的类扩展并在里面编写即可。

最后,借用一张来源网络的流程图作为收尾

消息转发.png