之前写了篇关于消息发送的文章,其中提及到了消息转发,由于 OC 是一门动态的语言,在编译时,编译器还无法确定到底有没有这个方法,因为在运行时我们还可以动态的给它添加对应的方法处理,我曾说过,当一个方法在自己或其继承结构上面的方法列表中都没有找到,就会启动对应的 "消息转发" 机制。
好,现在我们开始好好说说 "消息转发" 究竟是怎么一回事。总得来说,消息转发的流程分为两大阶段。先来说第一阶段,在第一阶段时,当接受者无法处理发送的消息时,首先会触发当前类的 resolveInstanceMethod: 或者对应的 resolveClassMethod: 方法,在这个方法中会询问接受者,是否能够动态的给它添加对应的方法来处理这个未知的消息,这个行为的专业术语叫 "动态方法解析",在这个方法中,系统会将未处理的那个 @selector(方法) 名传递给我们,然后让我们返回一个 BOOL 值,询问是否能够添加一个实例或者类方法来处理它,当我们直接返回一个 NO ,或者直接调用父类方法让父类处理它时,自然就会崩掉,举个例子:
Person *p = [[Person alloc]init];
objc_msgSend(p,@selector(eat));
在上面的代码中,Person 这个类里是没有 eat 这个实例方法的,再看下面:
@implementation Person
+ (BOOL)resolveInstanceMethod:(SEL)sel{
return [super resolveInstanceMethod:sel];
}
@end
在上面的代码中,我直接调用了父类的处理方法,当然,这其实和不写是没有区别的,我们现在看看运行后的报错信息
reason: '-[Person eat]: unrecognized selector sent to instance 0x7ff64350fdf0'
不管英语是好是差,作为一个程序员,看到这种报错信息,哪怕你不懂英语也能看出来错误原因了吧 "向实例发送了一个未识别的消息" ,那个 0x7ff64350fdf0 也就是当前 p 对象的地址,其实报错信息里还有一条重要的信息:
0 CoreFoundation 0x0000000105896d85 __exceptionPreprocess + 165
1 libobjc.A.dylib 0x000000010530adeb objc_exception_throw + 48
2 CoreFoundation 0x000000010589fd3d -[NSObject(NSObject) doesNotRecognizeSelector:] + 205
其中的 [NSObject(NSObject) doesNotRecognizeSelector:] + 205 这个方法应该说是消息传递过程中的最后一步,当没有任何措施处理这个未识别的消息,便会调用 NSObject的doesNotRecognizeSelector:方法默认实现来抛出异常。
那么,如何在第一阶段来拦截这个消息转发的过程,进行 "动态方法解析呢" ? OK,现在我们来试一下:
@implementation Person
void change(){
NSLog(@"我是被添加的方法");
}
+ (BOOL)resolveInstanceMethod:(SEL)sel{
class_addMethod(self, sel, (IMP)change, "v@:");
return YES;
}
@end
在上面的代码中,我给它创建了一个 change 函数,在 resolveInstanceMethod 方法中给它添加进了 class_addMethod 这个函数,这个函数中,第一个参数是需要添加方法的类,第二个参数是需要添加的方法名,第三个参数是指向函数的IMP指针,第四个参数是待添加方法的类型编码(类型编码:编码开头的字符表示方法的返回值类型,后面的字符表示各个参数),并且将 resolveInstanceMethod: 方法返回了 YES ,表示能够动态的添加方法来处理这个未识别的消息,这样一来,控制台便会打印 change 函数中打印的那段字符串。
好了,刚刚前面部分其实说的都是第一阶段,那部分报错信息应该说是第二阶段的末尾部分,大家先别关注,接着往下看。
现在说第二阶段,如果在第一阶段时,接收者没有进行 "动态方法解析" ,那么就会进入第二阶段,这时接收者已经无法再利用动态添加方法来处理未识别的消息了,但是,这时候接收者也可以用其他手段来处理这条消息,这时的第二阶段又得分为两步,第一步,运行时系统会询问接收者是否有其他对象来帮助其处理这条消息, 具体会调用当前类的如下方法进行询问:
- (id)forwardingTargetForSelector:(SEL)aSelector{
}
从上面的代码中可以看到,该方法会返回一个 id 类型的对象,意思就是,如果有其他对象可以帮助它处理这条消息,就将那个对象返回,如果没有,就返回 nil,但是,请注意,刚刚也说了,在这第二阶段,已经没有办法在这里给它动态添加方法了,所以千万不要把其写在这个方法中。
经常刚刚的第二阶段的第一步操作后,如果已经返回了可以处理该消息的对象,那么该消息就被处理,过程便结束。但是如果返回的是 nil, 那么就会进入到第二步,也就是启动 "完整的消息转发机制" 了,这时,首先会创建一个 NSInvocation 类型的对象,可以点到头文件去看,这个对象里包含了 selector 、target(调用目标) 以及参数,当创建了这个对象,运行时会把消息传递给这个对象中,并且调用下面的方法来转发消息:
- (void)forwardInvocation:(NSInvocation *)anInvocation{
}
在上面的代码中,你只需要在实现里改变 anInvocation 对象的 target (调用目标) 属性,让消息的在新的目标上得到实现即可,其实这个跟上面的第一步是差不多的,也很少有人用到这些,如果依然没有新的目标来处理这个消息,这个类就会按照继承体系,一直向上寻找有没有父类及以上处理了forwardInvocation:这个方法,直至 NSObject 类,如果一直到NSObject 类之前都没有哪个类来处理,那么调用完 NSObject 类这个方法后,继而调用 NSObject 类的 doesNotRecognizeSelector 方法,抛出异常,表明这个消息最终没有被处理。
消息转发部分我要说的基本就这么多,下面这张图很好的表达了消息转发的整个过程:
其实,我们在开发中基本上不需要管第二阶段的那些流程,最好的方式都是在第一阶段中的动态添加方法中给它处理好,当然,笔者说的这些都是自己的看法,有不对的地方也希望各位能够指出,👋
网友评论