专栏首页iOS 开发杂谈iOS RunTime之四:消息转发

iOS RunTime之四:消息转发

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

  • 消息发送是 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对应的实现方法,所以返回了一个空的方法签名,最终导致程序报错崩溃。

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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • iOS RunTime之二:数据结构

    由上面一章中,我们了解了什么是RunTime,RunTime用来做什么,下面了解一下Runtime数据结构。

    s_在路上
  • 浅谈 AutoreleasePool 的实现原理

    在 MRC 的环境下,可以通过调用 [obj autorelease] 将对象添加到当前的 autoreleasepool 中,来延迟释放内存;

    s_在路上
  • 为什么数组都是从0开始编号

    为什么数组都是从 0 开始编号,首先先了解一下数组的概念。 数组 Array 是一种线性表数据结构,是一组连续的内存空间,用来存储一组具有相同类型的数据。数组...

    s_在路上
  • 理解消息转发机制

    王大锤
  • 理解消息转发机制

    王大锤
  • iOS底层原理总结 - 探寻Runtime本质(三)

    xx_Cc
  • vue 方法回调通知执行下一个方法

    在项目中有很多这样的需求,在一个方法执行完成拿到数据后才可以执行下一个方法,这就需要在第一个方法执行完后有个回调函数通知下一个方法可以执行了。

    honey缘木鱼
  • (4)OC中消息和消息转发-02

    czjwarrior
  • PHP——PDO

    PDO(PHP Data Object)是有MySql官方封装的、基于面向对象编程思想的、使用C语言开发的数据库抽象层。

    白胡杨同学
  • uni-app 网络请求

    若本号内容有做得不到位的地方(比如:涉及版权或其他问题),请及时联系我们进行整改即可,会在第一时间进行处理。

    达达前端

扫码关注云+社区

领取腾讯云代金券