iOS-消息转发机制详解

2,758 阅读11分钟

本文将会从消息转发机制的原理应用场景两个方向来进行讲解

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函数的调用过程)

下面这张图是消息转发的流程简图:

未命名文件(3).png 平时我们常见的错误: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,无额外的属性和方法),同时给FromNSObjectSubFromView添加调用的这两个函数的实现

-(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区别:

  1. 流程②转发的对象最多只能有一个,流程③却可以同时转发多个对象。
  2. 流程②只需要实现forwardingTargetForSelector这个方法,但是流程③必须同时实现methodSignatureForSelector和forwardInvocation方法。
  3. 流程②必须指定转发对象或者进入流程③,但是流程③作为最终步骤,可以不指定转发对象,也可以看心情要不要调用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

参考文章: