iOS 消息转发机制

消息转发(Message Forwarding)是在查找 IMP 失败后执行一系列转发流程的慢速通道,如果不作转发处理,则会打日志和抛出异常。

消息转发(Message Forwarding)是在查找 IMP 失败后执行一系列转发流程的慢速通道,如果不作转发处理,则会打日志和抛出异常开发过程中,通过- (id)performSelector:(SEL)aSelector调用了一个方法,经常会碰到这样的报错:unrecognized selector sent to instance **,原因是我们调用了一个不存在的方法。用OC消息机制来说就是:消息的接收者不过到对应的selector,这样就启动了消息转发机制,我们可以通过代码在消息转发的过程中告诉对象应该如何处理未知的消息,默认实现是抛出下面的异常:

对象方法

2019-02-11 17:48:20.387698+0800 MHYMessageForward[3707:151013] -[MHYPeople speak]: unrecognized selector sent to instance 0x600003981100
2019-02-11 17:48:20.392899+0800 MHYMessageForward[3707:151013] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[MHYPeople speak]: unrecognized selector sent to instance 0x600003981100'

类方法

2019-02-11 17:44:48.703348+0800 MHYMessageForward[3667:148858] +[MHYPeople speakClass]: unrecognized selector sent to class 0x1082d90a8
2019-02-11 17:44:48.708516+0800 MHYMessageForward[3667:148858] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '+[MHYPeople speakClass]: unrecognized selector sent to class 0x1082d90a8'

原理:

我们先看一下下面这个结构图,先对整个消息处理机制有一个初步的认识。

image

从全局来看,消息转发机制共分为3大步骤:

1.Method resolution 方法解析处理阶段

2.Fast forwarding 快速转发阶段

3.Normal forwarding 常规转发阶段

那么如果想要不抛出unrecognized selector 的报错,也就需要从这3步里面来做补救了,我们一步一步来看如何在这3个阶段来进行补救。

第一步:Method resolution 方法解析处理阶段
对象在收到无法解读的消息后,首先会调用+(BOOL)resolveInstanceMethod:(SEL)sel或者+ (BOOL)resolveClassMethod:(SEL)sel, 询问是否有动态添加方法来进行处理;如果YES则能接受消息, NO不能接受消息 进入第二步;

一、我们先调用一下对象方法:

[self.people performSelector:@selector(speak)];

然后在resolveInstanceMethod进行补救

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    //判断是否为外部调用的方法
    if ([NSStringFromSelector(sel) isEqualToString:@"speak"]) {
        /**
         对类进行对象方法 需要把方法添加进入类内
         */
        [MHYRuntimeTool addMethodWithClass:[self class] withMethodSel:sel withImpMethodSel:@selector(addDynamicMethod)];
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

当People 收到了未知 speak选择子的消息的时候,如果是实例方法会首选调用上文的resolveInstanceMethod:方法,方法内通过判断选择子然后通过class_addMethod方法动态添加了一个speak的实现方法来解决掉这条未知的消息,此时消息转发过程提前结束。
但是当People 收到speak 这条未知消息的时候,第一步返回的是No,也就是没有动态新增实现方法的时候就会调用第二步。

二、下面我们再来调用一下People的类方法

[[MHYPeople class] performSelector:@selector(speakClass)];

如果调用类方法需要在resolveClassMethod 进行补救判断

+ (BOOL)resolveClassMethod:(SEL)sel {
    //判断是否为外部调用的方法
    if([NSStringFromSelector(sel) isEqualToString:@"speakClass"]){
        /**
         对类进行添加类方法 需要将方法添加进入元类内
         */
        [MHYRuntimeTool addMethodWithClass:[MHYRuntimeTool getMetaClassWithTargetClass:[self class]] withMethodSel:sel withImpMethodSel:@selector(addClassDynamicMethod)];
        return YES;
    }
    return [super resolveClassMethod:sel];
}

这里有一个需要特别注意的地方,类方法需要添加到元类里面,OC中所有的类本质上来说都是对象,对象的isa指向本类,类的isa指向元类,元类的isa指向根元类,根元类的isa指向自己,这样的话就形成了一个闭环(如下图:)。


指针查找.png
+ (Class)getMetaClassWithTargetClass:(Class)targetClass {
    //转换是字符串类别
    const char *classChar = [NSStringFromClass(targetClass) UTF8String];
     //需要char的字符串 获取元类
    return objc_getMetaClass(classChar);
}

这个方法是用来获取本类的元类,对元类添加需要添加的方法。

经过上面两种类型的补救,果然对象方法和类方法都不在抛出异常了,并且打印了数据

2019-02-11 17:41:49.365085+0800 MHYMessageForward[3623:146602] 动态添加类方法
2019-02-11 17:41:49.365233+0800 MHYMessageForward[3623:146602] 动态添加方法

第二步:Fast forwarding 快速转发阶段 (后面阶段都针对对象来处理,不考虑类方法)
既然第一步已经问过了,没有新增方法,那就问问有没有别人能够帮忙处理一下啊,调用的是- (id)forwardingTargetForSelector:(SEL)aSelector这个方法。
我们先把上面方法内的处理方案注释掉,让消息转发进入第二步。
我们新创建一个keepBackPeople类,里面声明和实现speak方法,用来当作备用响应者。

-(id)forwardingTargetForSelector:(SEL)aSelector{
    if ([NSStringFromSelector(aSelector) isEqualToString:@"speak"]) {
        return [keepBackPeople new];
    }
    return [super forwardingTargetForSelector:aSelector];
}

因为一个对象内部可能还有其他可能响应的对象,所以这个方法是转发SEL去对象内部的其他可以响应该方法的对象。
这里创建的一个keepBackPeople的实例内定义的有speak方法,所以返回这个实例之后,果然不再报错了,并且根据打印也能看得出来走了BackupTestMessage这个类的实例方法

2019-02-11 17:55:57.848245+0800 MHYMessageForward[3789:155754] 备用类的对象方法speak

已经让备用的对象去响应了People本身无法响应的一个SEL

第三步:Normal forwarding 常规转发阶段
如果第二步返回self或者nil,则说明没有可以响应的目标 则进入第三步。
第三步的消息转发机制本质上跟第二步是一样的都是切换接受消息的对象,但是第三步切换响应目标更复杂一些,第二步里面只需返回一个可以响应的对象就可以了,第三步还需要手动将响应方法切换给备用响应对象。
第三步有2个步骤:

(1)- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{}

在第(1)步中,返回SEL方法的签名,返回的签名是根据方法的参数来封装的。
手动创建签名 但是尽量少使用 因为容易创建错误 可以按照这个规则来创建
https://blog.csdn.net/ssirreplaceable/article/details/53376915

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    /*手动创建签名 但是尽量少使用 因为容易创建错误 可以按照这个规则来创建
        https://blog.csdn.net/ssirreplaceable/article/details/53376915
     
     //写法例子
     //例子"v@:@"
     //v@:@ v 返回值类型void;@ id类型,执行sel的对象;: SEL;@ 参数
     //例子"@@:"
     //@ 返回值类型id;@ id类型,执行sel的对象;: SEL
     
     */
    //如果返回为nil则进行手动创建签名
    if ([super methodSignatureForSelector:aSelector]==nil) {
        NSMethodSignature * sign = [NSMethodSignature signatureWithObjCTypes:"v@:"];
        return sign;
    }
    return [super methodSignatureForSelector:aSelector];
    
}
(2)-(void)forwardInvocation:(NSInvocation *)anInvocation

上方的第(1)步中如果调用返回有签名 则进入消息转发最后一步

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    // 创建备用对象
    keepBackPeople *keepBack = [keepBackPeople new];
    SEL sel = anInvocation.selector;
    
    //判断备用对象是否可以响应传递进来等待响应的SEL
    if ([keepBackPeople respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:keepBack];
    }else {
        //如果备用对象不能响应,则抛出异常
        [self doesNotRecognizeSelector:sel];
    }
}

在三个步骤的每一步,消息接受者都还有机会去处理消息。同时,越往后面处理代价越高,最好的情况是在第一步就处理消息,这样runtime会在处理完后缓存结果,下回再发送同样消息的时候,可以提高处理效率。第二步转移消息的接受者也比进入转发流程的代价要小,如果到最后一步forwardInvocation的话,就需要处理完整的NSInvocation对象了。

实际用途:
(1)为 @dynamic 实现方法
使用 @synthesize 可以为 @property 自动生成 getter 和 setter 方法(现 Xcode 版本中,会自动生成),而 @dynamic 则是告诉编译器,不用生成 getter 和 setter 方法。当使用 @dynamic 时,我们可以使用消息转发机制,来动态添加 getter 和 setter 方法。当然你也用其他的方法来实现。
https://www.jianshu.com/p/6b05ba0e81e0

(2)实现多重代理
利用消息转发机制可以无代码侵入的实现多重代理,让不同对象可以同时代理同个回调,然后在各自负责的区域进行相应的处理,降低了代码的耦合程度。
https://blog.csdn.net/kingjxust/article/details/49559091

(3)间接实现多继承
Objective-C本身不支持多继承,这是因为消息机制名称查找发生在运行时而非编译时,很难解决多个基类可能导致的二义性问题,但是可以通过消息转发机制在内部创建多个功能的对象,把不能实现的功能给转发到其他对象上去,这样就做出来一种多继承的假象。转发和继承相似,可用于为OC编程添加一些多继承的效果,一个对象把消息转发出去,就好像他把另一个对象中放法接过来或者“继承”一样。消息转发弥补了objc不支持多继承的性质,也避免了因为多继承导致单个类变得臃肿复杂。
https://www.jianshu.com/p/9601e84177a3
demo传送门

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

推荐阅读更多精彩内容