iOS RunTime之四:消息转发

作者: s_在路上 | 来源:发表于2016-09-21 10:26 被阅读1024次

    消息发送和消息转发流程可以概括为:

    • 消息发送是 Runtime 通过 selector 快速查找 IMP 的过程,有了函数指针就可以执行对应的方法实现;
    • 消息转发是在查找 IMP 失败后执行一系列转发流程的慢速通道,如果不作转发处理,则会打日志和抛出异常。

    消息转发三部曲:

    • 动态方法解析
    + (BOOL)resolveInstanceMethod:(SEL)sel
    + (BOOL)resolveClassMethod:(SEL)sel
    
    • 重定向
    - (id)forwardingTargetForSelector:(SEL)aSelector
    

    在消息转发机制执行前,Runtime 系统会再给我们一次偷梁换柱的机会,即通过重载 - (id)forwardingTargetForSelector:(SEL)aSelector 方法替换消息的接受者为其他对象:

    - (id)forwardingTargetForSelector:(SEL)aSelector
    {
        if(aSelector == @selector(mysteriousMethod:)){
            return alternateObject;
        }
        return [super forwardingTargetForSelector:aSelector];
    }
    
    • 转发
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
    - (void)forwardInvocation:(NSInvocation *)anInvocation;
    

    当动态方法解析不作处理返回 NO 时,消息转发机制会被触发。在这时forwardInvocation:方法会被执行。

    该消息的唯一参数是个 NSInvocation 类型的对象——该对象封装了原始的消息和消息的参数。我们可以实现 forwardInvocation: 方法来对不能处理的消息做一些默认的处理,也可以将消息转发给其他对象来处理,而不抛出错误。

    这里需要注意的是参数 anInvocation 是从哪的来的呢?其实在 forwardInvocation: 消息发送前,Runtime 系统会向对象发送 methodSignatureForSelector: 消息,并取到返回的方法签名用于生成 NSInvocation对象。所以我们在重写 forwardInvocation: 的同时也要重写 methodSignatureForSelector: 方法,否则会抛异常。

    当一个对象由于没有相应的方法实现而无法响应某消息时,运行时系统将通过 forwardInvocation: 消息通知该对象。每个对象都从 NSObject 类中继承了 forwardInvocation: 方法。然而,NSObject 中的方法实现只是简单地调用了 doesNotRecognizeSelector:。通过实现我们自己的 forwardInvocation: 方法,我们可以在该方法实现中将消息转发给其它对象。

    forwardInvocation: 方法就像一个不能识别的消息的分发中心,将这些消息转发给不同接收对象。或者它也可以象一个运输站将所有的消息都发送给同一个接收对象。它可以将一个消息翻译成另外一个消息,或者简单的”吃掉“某些消息,因此没有响应也没有错误。

    forwardInvocation: 方法也可以对不同的消息提供同样的响应,这一切都取决于方法的具体实现。该方法所提供是将不同的对象链接到消息链的能力。

    注意:
    forwardInvocation: 方法只有在消息接收对象中无法正常响应消息时才会被调用。 所以,如果我们希望一个对象将 negotiate 消息转发给其它对象,则这个对象不能有 negotiate 方法。否则,forwardInvocation: 将不可能会被调用。

    Paste_Image.png

    名词解析

    首先,了解一下下面的几个词:

    动态方法解析

    一般我们写代码的时候有可能会用到 @dynamic,例如:

    @dynamic propertyName;
    

    这表明我们会为这个属性动态提供存取方法,也就是说编译器不会再默认为我们生成 setget 方法,而需要我们动态提供。我们可以通过分别重载 resolveInstanceMethod:resolveClassMethod: 方法分别添加实例方法实现和类方法实现。

    因为当 Runtime 系统在 Cache 和方法分发表中找不到要执行的方法时, Runtime 会调用 resolveInstanceMethod:resolveClassMethod: 来给程序员一次动态添加方法实现的机会。

    我们需要用 class_addMethod 函数完成向特定类添加特定方法实现的操作:

    void dynamicMethodIMP(id self, SEL _cmd) {
        // implementation ....
    }
    
    + (BOOL)resolveInstanceMethod:(SEL)aSEL
    {
        if (aSEL == @selector(resolveThisMethodDynamically)) {
              class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
              return YES;
        }
        return [super resolveInstanceMethod:aSEL];
    }
    

    注意:

    • v@:表示每一个方法会默认隐藏两个参数,self_cmdself 代表方法调用者,_cmd 代表这个方法的 SEL,签名类型就是用来描述这个方法的返回值、参数的,v 代表返回值为void@ 表示 self: 表示 _cmd

    • 动态方法解析会在消息转发机制浸入前执行。如果 respondsToSelector:instancesRespondToSelector: 方法被执行,动态方法解析器将会被首先给予一个提供该方法选择器对应的 IMP 的机会。如果你想让该方法选择器被传送到转发机制,那么就让 resolveInstanceMethod: 返回 NO

    self和_cmd

    我们经常在方法中使用 self 关键字来引用实例本身,但从没有想过为什么 self 就能取到调用当前方法的对象吧。其实 self 的内容是在方法运行时被偷偷的动态传入的。

    在讲消息发送的时候,我们知道当 objc_msgSend 找到方法对应的实现时,它将直接调用该方法实现,并将消息中所有的参数都传递给方法实现,同时,它还将传递两个隐藏的参数:

    • 接收消息的对象(也就是 self 指向的内容)
    • 方法选择器(_cmd 指向的内容)

    之所以说它们是隐藏的是因为在源代码方法的定义中并没有声明这两个参数。它们是在代码被编译时被插入实现中的。尽管这些参数没有被明确声明,在源代码中我们仍然可以引用它们。在这两个参数中,self 更有用。实际上,它是在方法实现中访问消息接收者对象的实例变量的途径。

    而当方法中的 super 关键字接收到消息时,编译器会创建一个 objc_super 结构体:

    Paste_Image.png

    这个结构体指明了消息应该被传递给特定超类的定义。

    receiver 仍然是 self 本身,这点需要注意,因为当我们想通过 [super class] 获取超类时,编译器只是将指向 selfid 指针和 classSEL 传递给了 objc_msgSendSuper 函数,因为只有在 NSObject 类找到 class 方法,然后 class 方法调用 object_getClass(),接着调用 objc_msgSend(objc_super->receiver, @selector(class)),传入的第一个参数是指向 selfid 指针,与调用 [self class] 相同,所以我们得到的永远都是 self 的类型。

    接下来,我们要通过一个小例子来简单、通俗的理解一下什么是消息转发以及如何消息转发,希望看完这篇文章时大家会彻底的明白OC的消息。

    上一篇消息发送,我们知道Objective-C语言动态语言。比如Car这个对象里面只声明没有实现函数名为fly的函数,编译器编译的时候会不会通过呢。

    Paste_Image.png

    通过运行程序,可以看出在语言中Objective-C只声明并且没有实现方法编译器依然能够通过,但是运行期间则会因为获取不到实际执行的方法而抛出异常。

    消息转发验证

    Paste_Image.png

    1、动态解析
    我们在Car类的.m文件里面,通过上面介绍动态解析可以知道,可以重载resolveInstanceMethod:resolveClassMethod:方法分别添加实例方法实现和类方法实现。因为当Runtime系统在Cache和方法分发表中找不到要执行的方法时,Runtime会调用resolveInstanceMethod:resolveClassMethod:来给程序员一次动态添加方法实现的机会。

    Paste_Image.png

    2、重定向
    我们新建一个Person类,为了让运行时系统能够运行到forwardingTargetForSelector:方法,我们先在resolveInstanceMethod:中返回NO,代码如下:

    Paste_Image.png Paste_Image.png

    从运行结果中看出,我们执行[person fly]方法,控制台中打出Carrun方法,最终也实现了消息的转发。

    Person *person = [[Person alloc] init];
    [person fly];
    

    3、转发
    如果我们都不实现forwardingTargetForSelector,系统就会方法methodSignatureForSelectorforwardInvocation来实现转发,代码如下:

    Paste_Image.png

    从运行结果中看出,我们执行[person fly]方法,控制台中打出Carrun方法,最终也实现了消息的转发。

    注意:

    • methodSignatureForSelector用来生成方法签名,这个签名就是给forwardInvocation中的参数NSInvocation调用的。
    • unrecognized selector sent to instance,原来就是因为methodSignatureForSelector这个方法中,由于没有找到fly对应的实现方法,所以返回了一个空的方法签名,最终导致程序报错崩溃。

    以上就是消息的转发,如果有觉得上述我讲的不对的地方欢迎指出,大家多多交流沟通。

    相关文章

      网友评论

        本文标题:iOS RunTime之四:消息转发

        本文链接:https://www.haomeiwen.com/subject/jvtaettx.html