消息转发机制(message forwarding)及其应用场景

引言:OC是一种消息语言,OC对象调用方法,就是给对象发送消息,这个过程称为消息传递,那如果对象接收到了无法解读的消息,这时候要怎么处理呢?此时就用到了OC中的消息转发机制(message forwarding)。本文分为两部分,第一部分介绍消息转发机制的过程,第二部分介绍消息转发机制的应用场景。

一.消息转发机制过程:

消息转发一共有三步:

1.动态方法解析(Dynamic Method Resolution):

+ (BOOL)resolveInstanceMethod:(SEL)selector; ①

+ (BOOL)resolveClassMethod:(SEL)selector;②

如果对象收到无法解读的消息,首先会调用对象所属类上述两个类方法之一,询问是否能够动态添加无法解读的selector。上述两个类方法分别对应selector为对象方法和类方法的情况。这两个方法返回值为BOOL,表示是否能新增一个方法来处理此选择子。

代码示例:

    People*people = [[People alloc]init];

    [people performSelector:NSSelectorFromString(@"tonightEatChicken")];

People类的实例对象people执行tonightEatChicken方法,而People类中并没有该方法的实现,如果不做任何处理,程序运行,将会崩溃。而如果我们使用动态方法解析做如下处理:

+ (BOOL)resolveInstanceMethod:(SEL)sel {

    NSString *selString = NSStringFromSelector(sel);

    if([selString isEqualToString:@"tonightEatChicken"]) {

        //为当前类添加此方法

        class_addMethod(self, sel , (IMP)tonightEatChicken, "v@:@");

        returnYES;

    }

    return [super resolveInstanceMethod:sel];

}

void tonightEatChicken(id self,SEL_cmd) {

    NSLog(@"%@--%@今晚吃鸡",self,NSStringFromSelector(_cmd));

}

再运行,程序正常运行,控制台输出--tonightEatChicken今晚吃鸡。
这是消息转发机制的第一步,值得注意的是:这一步骤中动态添加的方法,将会被运行时系统缓存,如果People类的实例稍后接收到同样的选择子,则不会进入消息转发流程,直接在消息发送阶段完成,这可以理解为runtime系统的优化工作,减少了方法查找的步骤。

2.备援接收者(Replacement Receiver)

如果当前接收者没有在第一步动态方法解析中进行处理,则还有第二次机会处理该selector,具体方法如下:

- (id)forwardingTargetForSelector:(SEL)selector;

这个方法同样由NSObject声明,所有继承于NSObject的类,都可以实现这个方法。这个方法需要返回可以接收该selector的类对象或者实例对象,如果该selector为类方法,则返回类对象,否则,返回实例。
具体实现如下,我们新建Soldier类并实现该选择子对象的方法:

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if ([NSStringFromSelector(aSelector) isEqualToString:@"tonightEatChicken"]) {
        //    //return [Soldier class];aSelector为类方法,则返回类对象
        return [[Soldier alloc]init];//aSelector为实例方法,则返回类对象
    }
    return [super forwardingTargetForSelector:aSelector];
}
#import "Soldier.h"

@implementation Soldier

- (void)tonightEatChicken {
    NSLog(@"士兵今晚吃鸡");
}

@end

程序成功运行并输出"士兵今晚吃鸡"。
其实在这一步,程序员能操作的就是改变消息的接收对象,这种方式可以模拟多重继承。OC是不支持多重继承的,利用消息转发可以变相的实现。外界看起来,似乎是一个类同时实现了两个类的某个功能,其实只是利用了消息转发。

3.完整的消息转发机制(Full Forwarding Mechanism)

如果前两步都没有处理,那么来到第三步,这一步系统会创建一个NSInvocation对象把这个消息的所有信息(包括target,selector,参数以及返回值)包装起来。并通过- (void)forwardInvocation:(NSInvocation *)anInvocation方法,把包装好的NSInvocation抛出来。
但是在创建NSInvocation对象之前,需要前获取这个消息的方法签名,通过

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if ([NSStringFromSelector(aSelector) isEqualToString:@"tonightEatChicken"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    }
    return [NSMethodSignature methodSignatureForSelector:aSelector];
}

然后再实现

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    [anInvocation invokeWithTarget:[[Soldier alloc]init]];
    NSLog(@"%@",anInvocation);
}

如此,就会将此消息转发给Soldier,控制台会打印出“士兵今晚吃鸡”,实现和第二步一样的效果。

小结:

接收者在每一步均有机会处理消息,步骤越往后,处理消息的代价越大。最好能在第一步就处理完,这样的话,运行期系统就可以将此方法缓存起来,如果该类的实例稍后收到同名选择子,就无须启动消息转发流程。如果只是想改变消息的接收者,那么在第三步操作不如在第二步操作。相对于第二步,第三步还会创建并处理完整的NSInvocation。

二.应用场景:

了解了技术的原理,就要考虑下,这个东西能用来干啥。下面介绍下书上和网络上有关消息转发的应用场景:

1.JSPatch

JSPatch是一个热修复的第三方开源库。它的实现原理就是利用了消息转发机制。
具体来说,JSPatch是利用了第三步的NSInvocation对象,因为在消息转发的第一步和第二步,我们只能获取消息的选择子,而在第三步,我们可以通过NSInvocation获取当前消息的所有内容(接收者,选择子,参数值)。因此可以在第三步,获取参数值。
JSPatch具体是怎么做的呢?JSPatch 的基本原理就是:JS 传递字符串给 OC,OC 通过 Runtime 接口调用和替换 OC 方法。

2.实现属性的自动化存取

这里模仿实现一个《Effective Objective-C 2.0》书中描述的一个完整的例子:
下面示范如何用动态方法解析来实现@dynamic属性。实现一个“字典”对象,内部可以用字典存取其他对象,但是存取方式,要通过属性的set和get方式来实现。开发者只需要声明属性,并将属性声明为@dynamic。这样运行时系统就不会自动为属性生成相应的set和get方法,需要开发者自己去实现。如果属性比较少,我们可以手动书写相应的存取方法:

#import 
@class People;

@interface MFExampleDictionary : NSObject

@property (nonatomic, copy) NSString *name;

@property (nonatomic, strong) People *people;

@end
#import "MFExampleDictionary.h"

@interface MFExampleDictionary ()
@property (nonatomic, strong) NSMutableDictionary *storeDictionary;
@end

@implementation MFExampleDictionary
@dynamic name,people;

- (instancetype)init
{
    self = [super init];
    if (self) {
        _storeDictionary = [NSMutableDictionary dictionary];
    }
    return self;
}

- (void)setName:(NSString *)name {
//    使用属性的set方法名为key -> setName:
    NSString *key = NSStringFromSelector(_cmd);
    NSLog(@"%@",key);
    [_storeDictionary setObject:name forKey:key];
}

- (NSString *)name {
//    使用属性的set方法名为key -> setName:
    NSString *get = NSStringFromSelector(_cmd);
    NSString *key = getToSet(get);
    NSLog(@"%@",key);
    return [_storeDictionary objectForKey:key];
}

- (void)setPeople:(People *)people {
    NSString *key = NSStringFromSelector(_cmd);
    [_storeDictionary setObject:people forKey:key];
}

- (People *)people {
    NSString *get = NSStringFromSelector(_cmd);
    NSString *key = getToSet(get);
    NSLog(@"%@",key);
    return [_storeDictionary objectForKey:key];
}

NSString *getToSet(NSString *get) {
    NSString *firstChar = [get substringToIndex:1];
    NSString *upString = [get stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:[firstChar uppercaseString]];
    NSString *setString = [NSString stringWithFormat:@"set%@:",upString];
    return setString;
}

@end

如果需要存取的属性多达几百个呢?我们就需要编写大量的存取的方法。这时候自动转发机制就可以为我们所用了。直接看代码(头文件代码不变):

#import "MFExampleDictionary.h"
#import 

@interface MFExampleDictionary ()
@property (nonatomic, strong) NSMutableDictionary *storeDictionary;
@end

@implementation MFExampleDictionary
@dynamic name,people;

- (instancetype)init
{
    self = [super init];
    if (self) {
        _storeDictionary = [NSMutableDictionary dictionary];
    }
    return self;
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSString *selString = NSStringFromSelector(sel);
    if ([selString hasPrefix:@"set"]) {
        class_addMethod([self class], sel, (IMP)setMethod, "v@:@");
    }else{
        class_addMethod([self class], sel, (IMP)getMethod, "v@:");
    }
    return [super resolveInstanceMethod:sel];
}

void setMethod(MFExampleDictionary *self,SEL _cmd,id value) {
    NSString *key = NSStringFromSelector(_cmd);
    NSLog(@"%@",key);
    [self.storeDictionary setObject:value forKey:key];
}

id getMethod(MFExampleDictionary *self,SEL _cmd) {
    NSString *get = NSStringFromSelector(_cmd);
    NSString *key = getToSet(get);
    NSLog(@"%@",key);
    return [self.storeDictionary objectForKey:key];
}

NSString *getToSet(NSString *get) {
    NSString *firstChar = [get substringToIndex:1];
    NSString *upString = [get stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:[firstChar uppercaseString]];
    NSString *setString = [NSString stringWithFormat:@"set%@:",upString];
    return setString;
}

@end

我们可以看到,利用消息转发,完成了这种设计,并且减少了代码量。
在iOS的CoreAnimation框架中CALayer类就用了与本例相似的实现方式,这使得CALyer成为“兼容于键值编码的”容器类,也就是说,能够向里面随意添加属性,然后以键值对的形式来访问。于是开发者就可以向其中新增自定义的属性了,这些属性的存储工作由基类直接负责,开发者只需要在CALyer的子类中定义新属性即可。
tips:这个用法主要参考书中描述的用法,其实我个人有点迷惑,既然是存储是数据,为什么一定要在对象内部放一个字典的方式来解决呢?直接用属性对应的实例变量来存储岂不是更好?如果需要以字典的形式输出,完全可以用模型转字典的方式来代替完成。所以对应这种用法的必要性有点质疑,如果有哪位同学有不一样的想法,欢迎留言指点!

3.模拟多重继承

模拟多重继承,其实就是利用第二步和第三步来实现的。此种应用场景也不常见,花里胡哨,个人感觉有点鸡肋(=、=)。

小结:

上述的原理,我们能这么干,说白了,还是苹果爸爸暴露出来的API,苹果爸爸给我们什么,我们用什么。值得思考的一点是,消息转发机制有什么作用呢?Apple为什么要这么设计呢?防止收到未知消息而崩溃吗?若是为了防止崩溃,必须提前知道哪些方法没有被实现,那么既然已经知道了,程序员在编程的时候在相应的类添加一下方法实现不就行了吗?为什么还要多此一举呢?
关于消息转发的应用场景目前就介绍这么多,我看网上有博客说还可以用来实现“多重代理”,这个有待考证。
如果哪位朋友关于消息转发有更深的认识,欢迎指教!

你可能感兴趣的:(消息转发机制(message forwarding)及其应用场景)