5. Runtime消息转发
在3. 消息机制的基本原理最后一步我们提到:若找不到对应的selector,消息被转发或者临时向receiver
添加这个selector对应的实现方法,否则就会崩溃
当一个方法找不到的时候,Runtime提供了消息动态解析、消息接受者重定向、消息定向等三步处理消息,具体流程如下
image
5.1 消息动态解析(动态添加方法)
Objective-C运行时会调用+resolveClassMethod
或+resolveInstanceMethod
,让你有机会提供一个函数实现。前者在对象方法未找到时调用,后者在类方法未找到时调用。我们可以通过重写这两个方法,添加其他函数实现,并返回YES,那运行时系统就会重新启动一次消息发送的过程
主要用到的方法如下
动态解析的方法位于
// 位于objc/NSObject.h + (BOOL)resolveClassMethod:(SEL)sel ; + (BOOL)resolveInstanceMethod:(SEL)sel; //位于 objc/runtime.h /** * 向一个类添加新方法,此方法需要给定名称及参数 * * @param cls 要被添加方法的类 * @param name selector方法名称 * @param imp 实现方法的函数指针 * @param types 只想函数的返回值与参数类型 * * @return 如果添加方法成功返回YES,否则返回NO */ OBJC_EXPORT BOOL class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types) ;
代码示例:
// // ViewController.m // RuntimeDemo // // Created by Terence on 2021/5/10. // Copyright © 2021年 Terence. All rights reserved. // #import "ViewController.h" #import "objc/runtime.h" @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; [self performSelector:@selector(eat)]; } + (BOOL)resolveInstanceMethod:(SEL)sel { if (sel == @selector(eat)) { class_addMethod(self.class, sel, (IMP)eatMethod, "v@:"); return YES; } return [super resolveInstanceMethod:sel]; } void eatMethod(id obj, SEL _cmd) { NSLog(@"eat food"); } @end
输出结果:
2021-05-10 23:10:23.110858+0800 RuntimeDemo[3451:122697] eat food
从上边的例子中,我们可以看出,虽然我们没有实现fun方法,但是通过重写resolveInstanceMethod
方法,利用class_addMethod
方法动态的添加了对象方法eatMethod
,并执行,成功调用了eatMethod
方法
class_addMethod方法中的特殊参数
v@:
,可参考苹果官方文档中关于Type Encodings
的说明:Type Encodings
5.2 消息动态转发
如果上一步中+resolveClassMethod
、 +resolveInstanceMethod
没有添加其它函数实现,运行时就会进行到下一步:消息接收者重定向
如果当前对象实现了- forwardingTargetForSelector:
或+forwardingTargetForSelector:
方法,Runtime就会调用这个方法,允许我们将消息的接收者转发给其它对象
// 重定向类方法的消息接收者,返回一个类或实例对象 + (id)forwardingTargetForSelector:(SEL)aSelector; // 重定向方法的消息接收者,返回一个类或实例对象 - (id)forwardingTargetForSelector:(SEL)aSelector;
注意:
- 类方法和对象方法消息转发第二步调用的方法不一样,前者是
+forwardingTargetForSelector
方法,后者是-forwardingTargetForSelector
方法
- 这里
-resolveClassMethod:
或者-resolveInstanceMethod
无论是返回YES还是NO,只要其中没有添加其它函数实现,运行时都会进行下一步
代码示例:
@implementation Person - (void)eatFood:(NSString *)foodName { NSLog(@"person eat food : %@", foodName); } - (void)personSleep { NSLog(@"person is sleeping..."); } @end
@implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; [self performSelector:@selector(personSleep)]; } + (BOOL)resolveClassMethod:(SEL)sel { return YES; } + (BOOL)resolveInstanceMethod:(SEL)sel { return YES; } - (id)forwardingTargetForSelector:(SEL)aSelector { if (aSelector == @selector(personSleep)) { return [[Person alloc] init]; } return [super forwardingTargetForSelector:aSelector]; } @end
打印输出:
2021-05-11 11:50:55.352147+0800 LoadInitializeDemo[47468:1985216] person is sleeping...
可以看到,虽然当前ViewController
没有实现fun方法,+resolveInstanceMethod:
也没有添加其它函数实现,但是我们通过forwardingTargetForSelector
把当前ViewController
的方法转发给了person
对象去执行了
我们通过forwardingTargetForSelector
可以修改消息的接收者,该方法返回参数是一个对象,如果这个对象不是nil,也不是self,系统会将运行的消息转发给这个对象执行。否则,继续进行下一步:消息重定向流程
5.3 消息重定向
如果经过消息动态解析、消息接收者重定向,Runtime系统还是找不到相应的方法实现而无法响应消息,Runtime系统会利用-methodSignatureForSelector:
或+methodSignatureForSelector:
方法获取函数的参数和返回值类型
- 如果
methodSignatureForSelector
返回了一个NSMethodSignature
对象(函数签名),Runtime系统就会创建一个NSInvocation
对象。并通过forwardInvocation:
消息通知当前对象,给予此次消息发送最后依次寻找IMP的机会
- 如果
methodSignatureForSelecotr:
返回nil,则Runtime系统会发出doesNotRecognizeSelector:
消息,程序也就崩溃了
所以我们可以在forwardingInvocation:
方法中对消息进行转发
注意:类方法和对象方法消息转发第三步调用的方法同样不一样
类方法调用的是:
+methodSignatureForSelector
+ forwardInvocation:
doesNotRecognizeSelector:
对象方法调用的是
-methodSignatureForSelector:
-forwardingInvocation:
doesNotRecognizeSelector:
用到的方法
//获取类方法函数的参数和返回值类型,返回签名 + (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector; // 类方法消息重定向 + (void)forwardInvocation:(NSInvocation *)anInvocation { NSLog(@"aInvocation: %@", anInvocation); // 获取对象方法函数的参数和返回值类型,返回签名 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector; // 对象方法消息重定向 - (void)forwardInvocation:(NSInvocation *)anInvocation { NSLog(@"aInvocation: %@", anInvocation); } }
代码示例
#import "ViewController.h" #import "Person.h" #import <objc/runtime.h> @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; [ViewController performSelector:@selector(personWakeup)]; } + (BOOL)resolveInstanceMethod:(SEL)sel { //为了进行下一步,消息接收者重定向 return YES; } //消息接收者重定向 + (id)forwardingTargetForSelector:(SEL)aSelector { //为了进行下一步,消息重定向 return [super forwardingTargetForSelector:aSelector]; } // 获取函数的参数和返回值类型,返回签名 + (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { if ([NSStringFromSelector(aSelector) isEqualToString:@"personWakeup"]) { return [NSMethodSignature signatureWithObjCTypes:"v@:"]; } return [super methodSignatureForSelector:aSelector]; } // 消息重定向 + (void)forwardInvocation:(NSInvocation *)anInvocation { NSLog(@"aInvocation: %@", anInvocation); SEL sel = anInvocation.selector; if ([Person respondsToSelector:sel]) { //判断Person类对象是否可以响应sel [anInvocation invokeWithTarget:Person.class]; // 若可以响应,则将消息转发给其它对象处理 } else { [anInvocation doesNotRecognizeSelector:sel];//若仍然无法响应,则报错:找不到方法 } } @end
打印结果:
2021-05-11 16:40:10.119025+0800 LoadInitializeDemo[93832:2252330] person will wake up...
可以看到,我们在+forwardingInvocation:
方法里面让Peron
对象去执行了personWakeup
函数
既然-forwardingTargetForSelector:
和-forwardingInvocation:
都可以将消息转发给其它对象处理,那么两者区别在哪?
区别就在于-forwardingTargetForSelector:
只能将消息转发给一个对象,而 -forwardingInvocation:
可以将消息转发给多个对象
以上就是Runtime消息转发的整个流程
结合之前讲的3.消息机制的基本原理,就构成了整个消息发送及转发的流程,下面我们来总结下整个流程
6. 消息发送一级转发机制总结
调用[receiver selector]后,进行的流程:
- 编译阶段:[receiver selector]方法被编译器转换为:
objc_msgSend(receiver, selector)
(不带参数)objc_msgSend(receiver,selector, org1, org2, ...)
(带参数)
- 运行时阶段:消息接收者receiver寻找对应的selector
- 通过
receiver
的isa
指针找到receiver
的Class
- 在Class的cache(方法缓存)的散列表中寻找对应的IMP(方法实现)
- 如果在
cache(方法缓存)
中没有找到对应的IMP(方法实现)
,则继续在Class(类)
的methodLists
中寻找对应的selector
,如果找到,填充到cache(方法缓存)
中,并返回selector
- 如果在
class(类)
中没有找到这个selector
,就继续在它的superclass(父类)
中找 - 一旦找到对应的
selector
,直接指向receiver
对应的selector
方法实现的IMP(方法实现)
- 若找不到对应的
selector
,Runtime
系统进入消息转发机制
3.运行时消息转发阶段: - 动态解析:通过重写
resolveInstanceMethod
或resolveClassMethod
,利用class_addMethod
动态添加方法 - 消息接收者重定向:如果上一步没有添加其它函数实现,可在当前对象中利用
forwardingTargetForSelector
将消息的接收者转给其它对象 - 消息重定向: 如果上一步没有返回值为nil,返回了一个NSMethodSignature对象(函数签名),Runtime系统就会创建一个NSIncovation对象,并通过
forwardingInvocation:
消息通知当前对象,给予此次消息发送最后依次寻找IMP的机会 - 如果methodSignationForSelector返回nil,则Runtime系统就会发出
doesNotRecoginzerSelector:
消息,程序也就崩溃了