Objective-C消息转发机制

前言:最近一直在研读《Effective Objective-C 2.0》这篇文章,觉得受益匪浅。自己将书上的代码进行实现,再结合作者的解释进行思路扩展之后,对oc语言机制有了更深的理解;

这篇文章结合了自身的理解,尽量更清楚明白的阐述消息转发机制的实现过程和原理,如有阐述不当的地方,欢迎指导改正。


什么是消息转发?
oc中方法调用就是一个消息传递的过程。例如:

[self testMethodWithInfo:info];

本例中,self被称为“接收者”,testMethodWithInfo:被称为选择器,info为参数,选择器和参数合起来称为“消息”。

但是,在编译期像对象发送了其无法解读的消息之后,编译器并不会报错,只会给出一个方法名未知的警告:

屏幕快照 2017-08-01 下午4.14.13.png

这是因为在运行期可以继续向类中添加方法,所以编译器在编译时还无法确定类中到底会不会有某个方法实现。当对象接收到无法解读的消息后,就会启动“消息转发”机制,我们也可以经由此过程老告诉对象应该如何处理未知消息。

消息转发机制的实现过程:

消息转发分为两大阶段。第一个阶段先征询接收者所属的类,看其是否能动态添加方法,以处理当前这个“未知的选择器”,这叫做“动态方法解析”。第二个阶段涉及“完整的消息转发机制”。如果运行期征询结果接收者没能动态添加方法以响应包含该选择器的消息,此时系统会请求接收者以其他手段来处理与消息相关的方法调用。首先,请接收者看看有没有其他对象能处理这条消息,如果有,系统会把消息转发给那个对象,消息转发过程结束。如果没有“备援的接受者”,则启动完整的消息转发机制,系统会把与消息相关的全部细节都封装在NSInvocation对象中,再给接收者最后一次机会,令其设法解决当前还未处理的这条消息。

动态方法解析:
对象在收到无法解读的消息后,首先会调用所属类的下列类方法:

+ (BOOL)resolveInstanceMethod:(SEL)sel

参数sel就是未知的选择器,返回值BOOL类型,表示这个类能否新增一个实例方法以处理此选择器。假如将要实现的不是实例方法而是类方法,系统则会调用另外一个方法:

+ (BOOL)resolveClassMethod:(SEL)sel

使用这种办法的前提是:相关方法的实现代码已经写好(否则识别不到新添加的方法名),只等着运行的时候动态插入到类里面就可以了。
演示代码:
先实现替代方法:

#import "StudentModel.h"

void nullMethodSubstite(id self, SEL _cmd){
    
    NSLog(@"message has transpond");
    
};

然后在+ (BOOL)resolveInstanceMethod:(SEL)sel方法中进行处理:

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    
    NSString *str = NSStringFromSelector(sel);
    if ([str containsString:@"nullMethod"]) {
        
        
        /**
         通过给定名称向类中添加新方法

         @param self 指定类
         @param sel 待处理的选择器
         @param IMP 方法名
         @return 用来描述方法参数类型的字符集
         */
        class_addMethod(self, sel, (IMP)nullMethodSubstite, "@@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

1、获取选择器;2、检测选择器是否表示nullMethod方法;3、向类中添加该方法
查看运行结果:

备援接收者:
当前接收者还有第二次机会能处理未知的选择器,在该步骤中,系统会征询接收者能不能把这条消息转给其他接收者来处理。对应的处理方法如下:

- (id)forwardingTargetForSelector:(SEL)aSelector

实现代码:

@interface ModelPrintViewController ()
{
//新建Student类
    StudentModel *_model;
}
@end
- (id)forwardingTargetForSelector:(SEL)aSelector{
    
    if ([NSStringFromSelector(aSelector) containsString:@"nullMethod"]) {
        return _model;
    }
    
    return [super forwardingTargetForSelector:aSelector];
}

在student类中实现代码:

- (void)nullMethod{
    NSLog(@"This is Student nullMethod");
}

1、判断是否为nullMethod方法;2、设置备援接收者;3、在备援接收者中实现该代码
查看运行结果:

屏幕快照 2017-08-01 下午5.18.32.png

注:我们无法操作经由这一步转发的消息,只能设置备援接收者替代当前对象来接收这一消息,如果想在发送给备援接收者之前先修改消息内容,那就得通过完整的消息转发机制来做了。

完整的消息转发:
如果转发算法已经来到这一步的话,那么唯一能做的就是启用完整的消息转发机制了。首先创建NSInvocation对象,把与尚未处理的那条消息相关的全部细节都封于其中。此对象包含选择器、目标(target)、及参数。在触发NSInvocation对象时,“消息派发系统将亲自出马,把消息指派给目标对象”。
此步骤会调用下列方法来转发消息:

- (void)forwardInvocation:(NSInvocation *)anInvocation

这个方法的实现可以很简单:只需改变调用目标,使消息在新目标上得以调用即可。然而这样实现出来的方法与“备援接收者“方案所实现的方法等效,所以很少有人采用这么简单的实现方式。比较有用的实现方式为:在触发消息前,先以某种方式改变消息内容,比如追加另外一个参数,或者改换选择器,等等。
实现此方法,若发现某调用操作不应由本类处理,则需调用超累的同名方法,这样的话,继承体系中的每个类都有机会处理此调用请求,直至NSObject。如果最后调用了NSObject类的方法,那么该方法还会调用”doesNotRecognizeSelector:“,以抛出异常,表明选择器最终未能得到处理。如图:

屏幕快照 2017-08-01 下午5.59.35.png

消息转发全流程:

屏幕快照 2017-08-01 下午6.03.04.png

上图描述了消息转发机制处理消息的各个步骤。

以完整的例子演示动态方法解析:

@dynamic: 使用@dynamic关键字声明属性,可以让编译器默认不去自动创建实现属性所用的实例变量,也不会为其创建存取方法,并且编译器不会报错。
为了说明消息转发机制的意义,下面示范如何以动态方法解析来实现@dynamic属性。
创建student对象,并且将其属性用@dynamic关键字进行声明后,在不创建实例变量和存取方法的情况下,通过消息转发机制实现其存取方法。示例如下:


#import <Foundation/Foundation.h>

@interface StudentModel : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *city;
@property (nonatomic, copy) NSString *country;



@end
#import "StudentModel.h"

@interface StudentModel ()
//muDict用来传值
@property (nonatomic, strong) NSMutableDictionary *muDict;

@end

@implementation StudentModel

@dynamic name```,city,country;

进行此操作后,编译器不会自动创建实例变量和存取方法,此时用点语法进行属性的存取值时,运行期系统找不到对应的选择器,此时消息转发机制启动,系统会调用所属类的+ (BOOL)resolveInstanceMethod:(SEL)sel方法,我们可以在此方法中对选择器进行处理:
首先实现替代方法:

id autoStudentGetter(id self, SEL _cmd){
    
    StudentModel *model = (StudentModel *)self;
    
    NSMutableDictionary *dict = model.muDict;
    NSString *key = NSStringFromSelector(_cmd);
    
    NSString *str = [[key substringFromIndex:3]lowercaseString];
    return [dict objectForKey:str];
}

void autoStudentSetter(id self, SEL _cmd, id value){
    
    StudentModel *model = (StudentModel *)self;
    NSMutableDictionary *dict = model.muDict;
    
    NSString *str = NSStringFromSelector(_cmd);
    NSMutableString *key = [str mutableCopy];
    
    [key deleteCharactersInRange:NSMakeRange(key.length-1, 1)];
    
    [key deleteCharactersInRange:NSMakeRange(0, 3)];
    
    NSString *lowerCharStr = [key lowercaseString];
    
    
    if (value && ([value isKindOfClass:[NSString class]] || [value isKindOfClass:[NSArray class]] || [value isKindOfClass:[NSNumber class]])) {
        
        [dict setObject:value forKey:lowerCharStr];
    }else{
        [dict removeObjectForKey:lowerCharStr];
    }
    
}

然后在+ (BOOL)resolveInstanceMethod:(SEL)sel方法中进行逻辑判断处理:

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    
    NSString *str = NSStringFromSelector(sel);
    if ([str containsString:@"set"]) {
        class_addMethod(self, sel, (IMP)autoStudentSetter, "v@:@");
    }else{
        class_addMethod(self, sel, (IMP)autoStudentGetter, "@@:");
    }
    return YES;
}

使用点语法给对象进行赋值,然后进行打印:

- (void)setData{
    _model = [[StudentModel alloc]init];
    _model.name = @"Samson";
    _model.city = @"FuYang";
    _model.country = @"China";
    
    NSLog(@"model.name:%@,model.city:%@,model.country:%@",_model.name,_model.city,_model.country);
}

查看打印结果:

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

推荐阅读更多精彩内容