消息转发

950 阅读4分钟

打印消息转发流程

在开始正文之前,首先介绍一个工具instrumentObjcMessageSends image.png

当打开instrumentObjcMessageSends, 那么每一个方法调用都会输出到"/tmp/msgSends-%d", (int) getpid ()这个文件中 image.png 对于后面探索消息转发流程起到重要的作用

消息转发对于未实现方法的处理

在上一章节探索了类对象方法的动态决议,但是在探索的过程中发现,当一个实例方法不存在的时候,resolveInstanceMethod:会被调用两次. 打开instrumentObjcMessageSends, 如下代码调用一个未实现的方法,看看调用过程是怎么样的 image.png image.png 观察到调用了两次resolveInstanceMethod:之后,又调用了两次forwardingTargetForSelector:, 此时比较奇怪,这个forwardingTargetForSelector:是什么,搜索苹果官方文档,给出的说明是

Returns the object to which unrecognized messages should first be directed.//返回未识别的消息首先应指向的对象如果一个对象实现(或继承)这个方法,并返回一个非nil(和非self)结果,则返回的对象将用作新的接收对象,消息分派将继续到该新对象。(显然,如果你从这个方法返回self,代码将陷入无限循环。)如果您在非根类中实现此方法,如果您的类对于给定的选择器没有任何返回,那么您应该返回调用super实现的结果。此方法使对象有机会在开销更大的forwardInvocation:机制接管之前,重定向发送给它的未知消息。当您只是想将消息重定向到另一个对象时,这是非常有用的,并且可以比常规转发快一个数量级。如果转发的目标是捕获NSInvocation,或者在转发期间操纵参数或返回值,那么它就没有用了。

那么forwardingTargetForSelector:给一个机会去重定向selreceiver, 属于快速转发. 尝试修复上面的crash,新建一个MLStudent并且实现sayInstanceMethod image.png

运行一下,没有任何crash,并且执行了MLStudentsayInstanceMethod方法. 查看之前crash的调用过程,还发现一个方法的调用methodSignatureForSelector:,这时就进入慢速转发流程, 搜索官方文档,给出如下说明:

Returns an NSMethodSignature object that contains a description of the method identified by a given selector.

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
该方法用于协议的实现。这个方法也用于必须创建NSInvocation对象的情况,例如在消息转发期间。如果对象维护委托或能够处理它没有直接实现的消息,则应重写此方法以返回适当的方法签名。需要跟`forwardInvocation:`搭配使用.

文档说明,我们可以返回一个NSMethodSignature包含这个sel的描述, 关于什么是方法签名,再次不做详细解释, 当我们添加如下代码到MLPerson,

image.png

控制台也没有任何关于sayInstanceMethod的调用打印。所有的方法,都统称为消息,同时也统称为事务(invocation),这个事务可做可不做,当返回了一个NSMethodSignature之后,并且空实现forwardInvocation:, 当前事务就不会执行

小结

当进入消息转发流程的过程中,如果这个sel没有任何实现,有如下方式可以避免crash

  1. 重写forwardingTargetForSelector: 返回一个实现这个sel的对象,进行重定向转发
  2. 重写methodSignatureForSelector:, 并且重写forwardInvocation:决定要不要执行当前事务, 可以空实现,也可以按照如下方法实现
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    MLStudent *student = [MLStudent alloc];
    if ([self respondsToSelector:anInvocation.selector])
    {
        [anInvocation invoke];
    }
    else if ([student respondsToSelector:anInvocation.selector])
    {
        [anInvocation invokeWithTarget:student];
    }
    else
    {
        [super forwardInvocation:anInvocation];
    }
}

doesNotRecognizeSelector

当调用一个未实现的方法,控制台会打印出如下消息,bt打印出当前的调用堆栈 image.png 可以看到,调用doesNotRecognizeSelector是通过___forwarding__, 但是___forwarding___这个符号是在CoreFoundation里面的, CoreFoundation源码地址, 下载到源码之后,搜索forwarding相关字段,并没有搜到任何相关的代码, 接下来我们借助一个反汇编工具Hopper, 这个工具可以通过汇编代码,反汇编出伪代码

image.png image.png image.png image.png image.png 从反汇编得到的伪代码,也解释了为什么methodSignatureForSelector:forwardInvocation:需要搭配使用,只有实现了methodSignatureForSelector:才会调用到forwardInvocation: 最终所有的能补救的流程都走完了,如果还是不行,就调用doesNotRecognizeSelector.

消息转发对于未实现方法的处理流程图

image.png

resolveInstanceMethod 为什么会执行两次

将断点打到resolveInstanceMethod:中, 停在第二次调用resolveInstanceMethod:, 打印出堆栈 image.png 发现第二次调用resolveInstanceMethod: 是在消息转发的流程。 第一次调用是在消息查找慢速流程中调用的.