美文网首页
消息转发机制(message forwarding)及其应用场景

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

作者: 2thousand17 | 来源:发表于2020-03-02 19:59 被阅读0次

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

    相关文章

      网友评论

          本文标题:消息转发机制(message forwarding)及其应用场景

          本文链接:https://www.haomeiwen.com/subject/itmeqhtx.html