本文将会从消息转发机制的
原理和应用场景两个方向来进行讲解
1、消息转发机制的原理
要明白消息转发机制,首先要搞清楚几个关键词的含义和区别:方法、选择器、消息
- 方法:方法也可以认为就是我们平时说的函数,称呼不同而已,如下所示,test就是一个返回为空,不带参数的方法(函数);
-(void)test{
NSLog(@"this is a method without params");
}
- 选择器:选择器实际上是表示函数一种特殊字符串,定义为SEL,该字符串通常被隐藏无法被可视化访问。例如平时写的
@selector(test),这就是获取test函数的SEL方式。 - 消息:实际上我们所写的OC函数最终都会被编译成C语言的函数,OC的函数调用也会转化成C语言的objc_msgSend函数调用,objc_msgSend函数中的参数我们可以理解为就是消息,函数调用的过程可以理解为消息的转发。objc_msgSend(转发者(id),选择器(SEL),参数,参数,...)
1.消息的转发过程(OC函数的调用过程)
下面这张图是消息转发的流程简图:
平时我们常见的错误:
unrecognized selector sent to instance 0x7fd719006f40其实就是整个流程一直走到了过程③仍没能找对能匹配该方法的实例或者类对象,就直接doesNotRecognizeSelector抛出异常。
接下来详细讲讲这个过程。
********下面的内容都是围绕着上面的这张流程图展开讲的,所以后面的内容请结合这张图来理解
1.1 方法寻找
其实讲的就是上图的左侧流程,在发起方法调用的时候,如果是实例方法调用,则会首先循环该类的实例方法列表,没找到就找父类,一直到根类NSObject;如果是类方法调用,则首先循环该类的元类的方法列表,没找到就继续找父元类,最后直到根元类。如果是上述过程能找到对应的方法则流程结束,如果是没有找到对应的方法,则进入右边的流程。
1.2 消息转发
如果进入右边的流程,就意味着目前方法调用处于异常处理的阶段了。该阶段有三个小的阶段,也意味着该异常的三次处理机会。
1.2.1 Method Resolution:resolveInstanceMethod、resolveClassMethod
这两个函数分别处理实例对象方法调用报错和类方法调用报错,接下来我们演示一下,我们新建一个类 FromView.h
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface FromView : UIView
@end
NS_ASSUME_NONNULL_END
FromView.m
#import "FromView.h"
@implementation FromView
@end
接下来我们调用以下方法。利用performSelector来调用方法和直接调用方法的区别可以看这里
FromView *fromView = [[FromView alloc]init];
[fromView performSelector:@selector(instanceMethodTest) withObject:@"instanceMethodTest"];
[FromView performSelector:@selector(classMethodTest) withObject:@"classMethodTest"]
运行完上述代码,此时会报一个熟悉的错误unrecognized selector sent to instance 0x7ff084705620。接下来我们实现消息转发的流程①,在FromView.m文件添加以下代码
+(BOOL)resolveInstanceMethod:(SEL)sel {
if ([NSStringFromSelector(sel) isEqualToString:@"instanceMethodTest:"]) {
Method method = class_getInstanceMethod([self class], @selector(addDynamicInstanceMethod:));
IMP methodIMP = method_getImplementation(method);
const char * types = method_getTypeEncoding(method);
class_addMethod([self class], sel, methodIMP, types);
return YES;
}
return [super resolveInstanceMethod:sel];
}
+(BOOL)resolveClassMethod:(SEL)sel {
if ([NSStringFromSelector(sel) isEqualToString:@"classMethodTest:"]) {
// 类方法都是存在元类中,所以添加方法需要往元类上添加
Class metaClass = object_getClass([self class]);
Method method = class_getClassMethod([self class], @selector(addDynamicClassMethod:));
IMP methodIMP = method_getImplementation(method);
const char * types = method_getTypeEncoding(method);
class_addMethod(metaClass, sel, methodIMP, types);
return YES;
}
return [super resolveClassMethod:sel];
}
-(void)addDynamicInstanceMethod:(NSString *)value {
NSLog(@"addDynamicInstanceMethod value = %@",value);
}
+(void)addDynamicClassMethod:(NSString *)value {
NSLog(@"addDynamicClassMethod value = %@",value);
}
我们在resolve的这两个函数中利用runtime动态添加了两个方法,使得成功运行并输出了结果。
输出结果:
2021-11-11 16:47:20.553244+0800 testClass[25034:6211939] addDynamicInstanceMethod value = instanceMethodTest
2021-11-11 16:47:20.553418+0800 testClass[25034:6211939] addDynamicClassMethod value = classMethodTest
resolve函数中,如果返回No,那么就会进入流程②,也就是快速转发过程;如果是返回yes,那么编译器则会重发一次刚才的消息,然后相当于重新调用了performSelector。因为我们已经通过resolve函数动态添加上了实例方法和类方法,此时重发消息就能正常响应了。
1.2.2 快速转发: Fast Rorwarding
此时我们继续调用下面两个方法,和之前一样,依旧会报
unrecognized selector sent to instance错误,因为这两个方法在resolve阶段并没有能处理,接下来就会进入流程②
[fromView performSelector:@selector(instanceMethodTestFastForwarding:) withObject:@"instanceMethodTestFastForwarding"];
[FromView performSelector:@selector(classMethodTestFastForwarding:) withObject:@"classMethodTestFastForwarding"];
Fast Rorwarding这个过程主要需要做的就是告诉编译器,当前有哪个实例对象或者类对象能够处理这个方法。 我们新建一个类,实现我们刚调用的两个方法,SubFromView.h文件中没有别的属性和方法,就不列出来了
#import "SubFromView.h"
@implementation SubFromView
-(void)instanceMethodTestFastForwarding:(NSString *)strValue {
NSLog(@"instanceMethodTestFastForwarding value=%@",strValue);
}
+(void)classMethodTestFastForwarding:(NSString *)strValue {
NSLog(@"classMethodTestFastForwarding value=%@",strValue);
}
@end
再在FromView.m中添加实现fast forwarding的方法
-(id)forwardingTargetForSelector:(SEL)aSelector {
if ([NSStringFromSelector(aSelector) isEqualToString:@"instanceMethodTestFastForwarding:"]) {
SubFromView * subFromView = [[SubFromView alloc]init];
if ([subFromView respondsToSelector:aSelector]) {
return subFromView;
}
}
return [super forwardingTargetForSelector:aSelector];
}
+(id)forwardingTargetForSelector:(SEL)aSelector {
if ([NSStringFromSelector(aSelector) isEqualToString:@"classMethodTestFastForwarding:"]) {
if ([SubFromView respondsToSelector:aSelector]) {
return [SubFromView class];
}
}
return [super forwardingTargetForSelector:aSelector];
}
此时再次运行,可以看到最终由我们新建的SubFromView调用这两个方法,也就是消息最终被转发给了SubFromView。
2021-11-11 16:47:20.553244+0800 testClass[25034:6211939] addDynamicInstanceMethod value = instanceMethodTest
2021-11-11 16:47:20.553418+0800 testClass[25034:6211939] addDynamicClassMethod value = classMethodTest
2021-11-11 16:47:20.553677+0800 testClass[25034:6211939] instanceMethodTestFastForwarding value=instanceMethodTestFastForwarding
2021-11-11 16:47:20.553823+0800 testClass[25034:6211939] classMethodTestFastForwarding value=classMethodTestFastForwarding
forwardingTargetForSelector这个函数中主要是需要返回一个可以相应该方法的对象,可以是类对象或者实例对象,只要能响应该方法即可。如果返回self或者nil那么就会进入流程③,也就是完整消息转发阶段
1.2.3 完整消息转发:Normal Forwarding
接下来我们接续调用下面函数,和之前一样,依旧会报unrecognized selector sent to instance错误,因为这两个方法在fast forwarding阶段并没有能处理,接下来就会进入流程③,完整的消息转发
[fromView performSelector:@selector(instanceMethodTestNormalForwarding:) withObject:@"instanceMethodTestNormalForwarding"];
[FromView performSelector:@selector(classMethodTestNormalForwarding:) withObject:@"classMethodTestNormalForwarding"];
此时我们再新建一个类FromNSObject(继承于NSObject,无额外的属性和方法),同时给FromNSObject和SubFromView添加调用的这两个函数的实现
-(void)instanceMethodTestNormalForwarding:(NSString *)strValue {
NSLog(@"instanceMethodTestNormalForwarding value=%@",strValue);
}
+(void)classMethodTestNormalForwarding:(NSString *)strValue {
NSLog(@"classMethodTestNormalForwarding value=%@",strValue);
}
然后向FromView.m中添加以下代码来实现Normal Forwarding
-(void)forwardInvocation:(NSInvocation *)anInvocation {
SEL selector = anInvocation.selector;
BOOL found = FALSE;
if ([NSStringFromSelector(selector) isEqualToString:@"instanceMethodTestNormalForwarding:"]) {
SubFromView * subFromView = [[SubFromView alloc]init];
FromNSObject * fromNSObject = [[FromNSObject alloc]init];
if ([subFromView respondsToSelector:selector]) {
[anInvocation invokeWithTarget:subFromView];
found = YES;
}
if ([fromNSObject respondsToSelector:selector]){
[anInvocation invokeWithTarget:fromNSObject];
found = YES;
}
// optional
if (!found) {
[self doesNotRecognizeSelector:selector];
}
}
}
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature * sign = [super methodSignatureForSelector:aSelector];
if (!sign) {
sign = [NSMethodSignature signatureWithObjCTypes:"v@:@"];
}
return sign;
}
+(void)forwardInvocation:(NSInvocation *)anInvocation {
SEL selector = anInvocation.selector;
BOOL found = FALSE;
if ([NSStringFromSelector(selector) isEqualToString:@"classMethodTestNormalForwarding:"]) {
if ([SubFromView respondsToSelector:selector]) {
[anInvocation invokeWithTarget:[SubFromView class]];
found = YES;
}
if ([FromNSObject respondsToSelector:selector]){
[anInvocation invokeWithTarget:[FromNSObject class]];
found = YES;
}
// optional
if (!found) {
[self doesNotRecognizeSelector:selector];
}
}
}
+(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature * sign = [super methodSignatureForSelector:aSelector];
if (!sign) {
sign = [NSMethodSignature signatureWithObjCTypes:"v@:@"];
}
return sign;
}
输出结果:
2021-11-11 17:53:29.823458+0800 testClass[26049:6266557] addDynamicInstanceMethod value = instanceMethodTest
2021-11-11 17:53:29.823565+0800 testClass[26049:6266557] addDynamicClassMethod value = classMethodTest
2021-11-11 17:53:29.823736+0800 testClass[26049:6266557] instanceMethodTestFastForwarding value=instanceMethodTestFastForwarding
2021-11-11 17:53:29.823860+0800 testClass[26049:6266557] classMethodTestFastForwarding value=classMethodTestFastForwarding
2021-11-11 17:53:29.824170+0800 testClass[26049:6266557] instanceMethodTestNormalForwarding value=instanceMethodTestNormalForwarding
2021-11-11 17:53:29.824272+0800 testClass[26049:6266557] instanceMethodTestNormalForwarding value=instanceMethodTestNormalForwarding
2021-11-11 17:53:29.824425+0800 testClass[26049:6266557] classMethodTestNormalForwarding value=classMethodTestNormalForwarding
2021-11-11 17:53:29.824513+0800 testClass[26049:6266557] classMethodTestNormalForwarding value=classMethodTestNormalForwarding
流程三需要同时实现methodSignatureForSelector和forwardInvocation两个函数,相当于是重新给该消息进行签名,然后调用forwardInvocation转发。
[NSMethodSignature signatureWithObjCTypes:"v@:@"];,这里的"v@:@"是苹果官方的类型定义,具体可参照官方文档
重点来了,既然流程②和流程③都是在转发给别的对象,那么流程②和流程③的区别是什么呢?
1.2.4 Normal Forwarding和Fast Rorwarding区别:
- 流程②转发的对象最多只能有一个,流程③却可以同时转发多个对象。
- 流程②只需要实现forwardingTargetForSelector这个方法,但是流程③必须同时实现methodSignatureForSelector和forwardInvocation方法。
- 流程②必须指定转发对象或者进入流程③,但是流程③作为最终步骤,可以不指定转发对象,也可以看心情要不要调用doesNotRecognizeSelector来控制抛异常。所以流程③实际上可以用作避免闪退,比如发现没有可转发的对象时,此时友好的弹一个错误提示,而不是直接就闪退,这样能极大程度的优化用户体验。
2、消息转发机制的应用场景
上面巴拉巴拉说了那么大一堆,肯定有不少人会疑惑这个消息转发机制有啥用?三个阶段的转发,说是为了避免闪退,那么我们要实现这三个阶段任何阶段,我们都是需要先知道异常方法的名字和参数类型,既然我们已经知道这些信息,那为啥不在调用的那个类中添好这些实例方法或者类方法,还需要这么繁琐写这么一堆来处理异常。我在了解消息转发机制之前也这么觉得,感觉很鸡肋。呃。。。。,实际上我现在也觉得有点鸡肋,哈哈,大概是境界没到,用不了这么高端的东西。
好吧,进入正题,先列举一下别人如何很好利用这个消息转发机制的。下面的内容参考自iOS开发·runtime原理与实践
2.1 防止特定的崩溃
正如我刚才讲流程③的时候提到的,流程③可以不处理这个消息转发,也不抛异常,完全忽略这个错误。那么我们可以新建一个NSObject的Category,然后重写流程③的那两个函数,那么就能防止因为函数调用这种错误闪退了,同时可以弹框显示本次异常的相关信息,在APP提测的时候对测试人员挺友好的,最起码不会一惊一乍告诉你又闪退了,提示信息还能协助修改bug。建议谨慎使用这种方式,毕竟是重写的根类,万一子类中额外处理了消息转发流程,那必定会有惊喜,而且这个提审,不知道会不会被拒,请知道的朋友留言告知
#import "NSObject+Resolve.h"
@implementation NSObject (Resolve)
+(BOOL)resolveInstanceMethod:(SEL)sel{
return NO;
}
-(id)forwardingTargetForSelector:(SEL)aSelector {
return self;
}
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature * sign = [NSMethodSignature signatureWithObjCTypes:"v@:"];
return sign;
}
-(void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"something wrong,method is %@",NSStringFromSelector(anInvocation.selector));
}
@end
2.2 不同版本系统方法兼容
最简单的例子就是
tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
至于实现的方式我建议查看iOS开发·runtime原理与实践,在文章目录3.2.1章节写的很清楚,我没有这么做过,毕竟加个系统版本判断还是更简单些,此处偷懒,不再详细写。
2.3 实现多继承
首先iOS是不支持多继承的,我猜想估计是苹果觉得多继承附带二义性造成的不严谨吧,所以才会造出严谨到Int+Double都不被允许的swift。多继承这个算是好坏参半吧,不好的地方就比如我刚才说的二义性,这个算是最大的问题吧,但是好处也很明显,我们可以同时继承多个父类,并获取这多个类的公开的属性和方法,不恰当的可以形容为一举多得。
下面列出demo代码,父类有Father和Mother,子类为Son,在Son.m中通过forwardingTargetForSelector消息转发,间接实现了多继承。
// Father.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@protocol FatherProtocol<NSObject>
-(void)fatherMethod;
@end
@interface Father : NSObject<FatherProtocol>
@end
NS_ASSUME_NONNULL_END
// Father.m
#import "Father.h"
@implementation Father
- (void)fatherMethod{
NSLog(@"this is fatherClass");
}
@end
------------------------------------------------------
// Mother.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@protocol MotherProtocol<NSObject>
-(void)motherMethod;
@end
@interface Mother : NSObject<MotherProtocol>
@end
NS_ASSUME_NONNULL_END
// Mother.m
#import "Mother.h"
@implementation Mother
- (void)motherMethod {
NSLog(@"this is motherClass");
}
@end
---------------------------------------------------
// Son.h
#import <Foundation/Foundation.h>
#import "Mother.h"
#import "Father.h"
NS_ASSUME_NONNULL_BEGIN
@interface Son : NSObject
@property (nonatomic,strong) Mother * mother;
@property (nonatomic,strong) Father * father;
@end
// Son.m
#import "Son.h"
@implementation Son
- (instancetype)init
{
self = [super init];
if (self) {
_mother = [[Mother alloc]init];
_father = [[Father alloc]init];
}
return self;
}
+(BOOL)resolveInstanceMethod:(SEL)sel {
return [super resolveInstanceMethod:sel];
}
-(id)forwardingTargetForSelector:(SEL)aSelector {
if ([_mother respondsToSelector:aSelector]) {
return _mother;
}
if ([_father respondsToSelector:aSelector]) {
return _father;
}
return [super forwardingTargetForSelector:aSelector];
}
@end
NS_ASSUME_NONNULL_END
/**
* 当然我觉换个方式写好像也实现了多继承,并且更容易理解,如下所示:
*/
// Son.h
#import <Foundation/Foundation.h>
#import "Mother.h"
#import "Father.h"
NS_ASSUME_NONNULL_BEGIN
@interface Son : NSObject<MotherProtocol,FatherProtocol>
@property (nonatomic,strong) Mother * mother;
@property (nonatomic,strong) Father * father;
@end
NS_ASSUME_NONNULL_END
// Son.m
#import "Son.h"
@implementation Son
- (instancetype)init
{
self = [super init];
if (self) {
_mother = [[Mother alloc]init];
_father = [[Father alloc]init];
}
return self;
}
- (void)motherMethod {
if ([_mother respondsToSelector:@selector(motherMethod)]) {
[_mother motherMethod];
}
}
- (void)fatherMethod {
if ([_father respondsToSelector:@selector(fatherMethod)]) {
[_father fatherMethod];
}
}
@end
参考文章: