消息发送和消息转发流程可以概括为:
- 消息发送是
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:
将不可能会被调用。
名词解析
首先,了解一下下面的几个词:
动态方法解析
一般我们写代码的时候有可能会用到 @dynamic
,例如:
@dynamic propertyName;
这表明我们会为这个属性动态提供存取方法,也就是说编译器不会再默认为我们生成 set
和 get
方法,而需要我们动态提供。我们可以通过分别重载 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
、_cmd
,self
代表方法调用者,_cmd
代表这个方法的SEL
,签名类型就是用来描述这个方法的返回值、参数的,v
代表返回值为void
,@
表示self
,:
表示_cmd
。 -
动态方法解析会在消息转发机制浸入前执行。如果
respondsToSelector:
或instancesRespondToSelector:
方法被执行,动态方法解析器将会被首先给予一个提供该方法选择器对应的IMP
的机会。如果你想让该方法选择器被传送到转发机制,那么就让resolveInstanceMethod:
返回NO
。
self和_cmd
我们经常在方法中使用 self
关键字来引用实例本身,但从没有想过为什么 self
就能取到调用当前方法的对象吧。其实 self
的内容是在方法运行时被偷偷的动态传入的。
在讲消息发送的时候,我们知道当 objc_msgSend
找到方法对应的实现时,它将直接调用该方法实现,并将消息中所有的参数都传递给方法实现,同时,它还将传递两个隐藏的参数:
- 接收消息的对象(也就是
self
指向的内容) - 方法选择器(
_cmd
指向的内容)
之所以说它们是隐藏的是因为在源代码方法的定义中并没有声明这两个参数。它们是在代码被编译时被插入实现中的。尽管这些参数没有被明确声明,在源代码中我们仍然可以引用它们。在这两个参数中,self
更有用。实际上,它是在方法实现中访问消息接收者对象的实例变量的途径。
而当方法中的 super
关键字接收到消息时,编译器会创建一个 objc_super
结构体:
这个结构体指明了消息应该被传递给特定超类的定义。
但 receiver
仍然是 self
本身,这点需要注意,因为当我们想通过 [super class]
获取超类时,编译器只是将指向 self
的 id
指针和 class
的 SEL
传递给了 objc_msgSendSuper
函数,因为只有在 NSObject
类找到 class
方法,然后 class
方法调用 object_getClass()
,接着调用 objc_msgSend(objc_super->receiver, @selector(class))
,传入的第一个参数是指向 self
的 id
指针,与调用 [self class]
相同,所以我们得到的永远都是 self
的类型。
接下来,我们要通过一个小例子来简单、通俗的理解一下什么是消息转发以及如何消息转发,希望看完这篇文章时大家会彻底的明白OC
的消息。
上一篇消息发送,我们知道Objective-C
语言动态语言。比如Car
这个对象里面只声明没有实现函数名为fly
的函数,编译器编译的时候会不会通过呢。
通过运行程序,可以看出在语言中Objective-C
只声明并且没有实现方法编译器依然能够通过,但是运行期间则会因为获取不到实际执行的方法而抛出异常。
消息转发验证
Paste_Image.png1、动态解析
我们在Car
类的.m
文件里面,通过上面介绍动态解析可以知道,可以重载resolveInstanceMethod:
和resolveClassMethod:
方法分别添加实例方法实现和类方法实现。因为当Runtime
系统在Cache
和方法分发表中找不到要执行的方法时,Runtime
会调用resolveInstanceMethod:
或resolveClassMethod:
来给程序员一次动态添加方法实现的机会。
2、重定向
我们新建一个Person
类,为了让运行时系统能够运行到forwardingTargetForSelector:
方法,我们先在resolveInstanceMethod:
中返回NO
,代码如下:
从运行结果中看出,我们执行[person fly]
方法,控制台中打出Car
的run
方法,最终也实现了消息的转发。
Person *person = [[Person alloc] init];
[person fly];
3、转发
如果我们都不实现forwardingTargetForSelector
,系统就会方法methodSignatureForSelector
和forwardInvocation
来实现转发,代码如下:
从运行结果中看出,我们执行[person fly]
方法,控制台中打出Car
的run
方法,最终也实现了消息的转发。
注意:
-
methodSignatureForSelector
用来生成方法签名,这个签名就是给forwardInvocation
中的参数NSInvocation
调用的。 -
unrecognized selector sent to instance
,原来就是因为methodSignatureForSelector
这个方法中,由于没有找到fly
对应的实现方法,所以返回了一个空的方法签名,最终导致程序报错崩溃。
以上就是消息的转发,如果有觉得上述我讲的不对的地方欢迎指出,大家多多交流沟通。
网友评论