Tips 1 - 消息转发的三板斧

2,702 阅读4分钟

前言

在 Objective-C 中,一个方法的调用,编译器会将其被转成 objc_msgSend 的方式,沿着这个对象或者类的继承链,依次去查找是否有对应的方法实现,如果查找至根类,也就是 NSObject 都没有找到对应的方法,那么就会触发消息转发流程。如果你在消息转发流程,还是没有进行处理,那么系统就会报错,程序终止。

这篇文章,只讲消息转发流程。(符合单一职责的设计模式,dog head)

三板斧

三板斧其实都是模板方法,只要实现对应的模板方法即可。

消息转发.png

第一斧:动态方法解析

首先,Runtime 会去调用 +resolveInstanceMethod:(实例方法) 或 +resolveClassMethod:(类方法),让你有机会去动态的添加一个方法,返回 YES,就会重启消息发送流程,再执行一次方法调用。

举个例子:

#import "ViewController.h"
#import <objc/runtime.h>

@interface Cat : NSObject

@end

@implementation Cat

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(fly)) {
        class_addMethod([self class], sel, (IMP)run, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

void run(id obj, SEL _cmd) {
    NSLog(@"Cat run");
}

@end

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Cat *cat = [[Cat alloc] init];
    [cat performSelector:@selector(fly)];
}

@end

尝试去调用 Cat 类中的 fly 方法,但是 Cat 中并没有 fly 方法,所以使用 class_addMethod 去动态的添加了一个方法,sel 依然是 fly,但是 IMP 改成了 run 方法。sel 可以理解为方法的名字,IMP 则是方法的实现,也就是这样处理之后,Cat 中被添加了一个名为 fly,但是实现却是 run 的方法,外界依然是通过 fly 来进行调用。

处理之后的打印结果:

Cat run

第二板斧:重定向

如果没有在第一步中,去动态的添加一个方法,那么就会执行到下一步,重定向,重定向意思是,你这没有这个方法,但是别人有,你让别人来实现这个方法。

例子:

#import "ViewController.h"
#import <objc/runtime.h>

@interface Bird : NSObject

@end

@implementation Bird

- (void)fly {
    NSLog(@"Bird fly");
}

@end

@interface Cat : NSObject

@end

@implementation Cat

//+ (BOOL)resolveInstanceMethod:(SEL)sel {
//    if (sel == @selector(fly)) {
//        class_addMethod([self class], sel, (IMP)run, "v@:");
//        return YES;
//    }
//    return [super resolveInstanceMethod:sel];
//}

//
//void run(id obj, SEL _cmd) {
//    NSLog(@"Cat run");
//}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(fly)) {
        return [[Bird alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}

@end

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Cat *cat = [[Cat alloc] init];
    [cat performSelector:@selector(fly)];
}

@end

输出是:

Bird fly

如果打开注释的代码,因为是先执行 resolveInstanceMethod: 方法,并且在其中添加的新的方法实现,所以不会走到 forwardingTargetForSelector:,输出是:

Cat run

不管 resolveInstanceMethod: 返回是 YES 还是 NO

第三板斧:完整的消息转发

最后一步像是一个那些不能被识别的消息的分发中心,它可以将这些消息转发给不同的对象,也可以将一个消息翻译成另外的一个消息,或者简单的吃掉某些消息,因此没有响应也没有错误,不会导致你的程序 crash。它也可以对不同的消息提供相同的响应,这一切都取决于方法的具体实现,该方法提供的是将不同对象连接到消息链的能力。

主要是两个方法,一个方法是 +(NSMethodSignature *)methodSignatureForSelector:,这个方法用来获取一个合适的方法签名,我们之前添加方法时用到的 v@: 就是一个方法的签名,具体的签名规则,可以看一下苹果的文档 Type Encodings,有很详细的说明。

如果要触发最后一个方法,这个签名必须是正确的函数签名,如果返回的是 nil,Runtime 会直接发出 -doseNotRecognizeSelector: 消息,程序直接 crash。

最后一个方法,就是 forwardInvocation: 方法,我们直接看例子:

#import "ViewController.h"
#import <objc/runtime.h>

@interface Bird : NSObject

@end

@implementation Bird

- (void)fly {
    NSLog(@"Bird fly");
}

@end

@interface Cat : NSObject

@end

@implementation Cat

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(fly)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    Bird *bird = [[Bird alloc] init];
    if ([bird respondsToSelector:anInvocation.selector]) {
        [anInvocation invokeWithTarget:bird];
    }
    else {
        [super forwardInvocation:anInvocation];
    }
}

@end

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Cat *cat = [[Cat alloc] init];
    [cat performSelector:@selector(fly)];
} 

@end

这里的处理,还是将消息转给 Bird 类去处理,但是你也可以做其他的实现,或者简单点,什么都不实现(不推荐),那么执行也不会报错。

总结

其实消息转发的流程是简单的,毕竟都有对应的模板方法,依次调用即可,这也是 Objective-C 是一门动态语言的一个体现,毕竟我们可以在运行时随意的调整方法的调用。

三板斧耍完了,该挨打了。

程咬金.jpeg

链接

Message Forwarding (官方文档)