消息转发(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'
原理:
我们先看一下下面这个结构图,先对整个消息处理机制有一个初步的认识。
从全局来看,消息转发机制共分为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指向自己,这样的话就形成了一个闭环(如下图:)。
+ (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传送门