消息转发机制(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));

}

再运行,程序正常运行,控制台输出<People: 0x6000023e06c0>--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 <Foundation/Foundation.h>
@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 <objc/message.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;
}

+ (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为什么要这么设计呢?防止收到未知消息而崩溃吗?若是为了防止崩溃,必须提前知道哪些方法没有被实现,那么既然已经知道了,程序员在编程的时候在相应的类添加一下方法实现不就行了吗?为什么还要多此一举呢?
关于消息转发的应用场景目前就介绍这么多,我看网上有博客说还可以用来实现“多重代理”,这个有待考证。
如果哪位朋友关于消息转发有更深的认识,欢迎指教!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,496评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,407评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,632评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,180评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,198评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,165评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,052评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,910评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,324评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,542评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,711评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,424评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,017评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,668评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,823评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,722评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,611评论 2 353

推荐阅读更多精彩内容