NSObject的消息转发机制

15年,刚开始接触runtime的时候,记录过消息转发相关的博客,最近做需求涉及到了一些消息转发相关的内容,得闲将旧博客翻出来重新梳理了一下。

为什么标题含有「NSObject」关键字眼呢?因为本文叙述的消息转发机制是针对NSObject对象;在我看来,Objective-C世界里的另一个根类NSProxy的消息转发逻辑和NSObject完全不同。

P.S: 如果没有特别说明,本文的NSObject指的是class NSObject,而不是protocol NSObject

如下这张取自《Effective Objective-C 2.0》的图片描述了NSObject对象的消息转发全流程:

QQ20150427-1.png

简单来说,当一个OC对象(receiver)接收到Unknown selector时,会进入如图流程,用户可以在这三个步骤中override receiver的相关方法,进而避免doesNotRecognizeSelector:异常。

这三次处理时机有何区别呢?《Effective Objective-C 2.0》的描述是:

步骤越往后,处理消息的代价就越大;最好能在第一步就处理完,这样的话,runtime系统就可以将此方法缓存起来,进而提高效率。若想在第三步里把消息转发给备援的receiver,那还不如把转发操作提前到第二步。因为第三步只是修改了调用目标,这项改动放在第二步会更为简单,不然的话,还得创建并处理完整的NSInvocation

本文旨在梳理这三个步骤。

+resolveInstanceMethod:

Receiver在收到unknown selector后,首先将调用其本类的resolveInstanceMethod:方法,该方法定义如下:

+ (BOOL)resolveInstanceMethod:(SEL)sel;

该方法的参数就是那个unknown selector,其返回值为Boolean类型,表示这个类是否能新增一个实例方法用以处理该unknown selector。在继续往下执行转发机制之前,本类有机会新增一个处理此selector的方法。所以resolveInstanceMethod:的一般使用套路是:

+ (BOOL)resolveInstanceMethod:(SEL)aSelector {
if (/* aSelector满足某个条件 */) {
/*
调用class_addMethod为该类添加一个处理aSelector的方法,譬如:
class_addMethod(self, aSelector, aImp, @"v@:@");
*/
return YES;
}
return [super resolveInstanceMethod:aSelector];
}

假如尚未实现的方法不是实例方法而是类方法,那么runtime系统会调用另外一个与resolveInstanceMethod:类似的方法resolveClassMethod:

就我经验而言,resolveInstanceMethod:的使用场景一般用来动态添加setter和getter。

-forwardingTargetForSelector:

当前receiver还有第二次机会能处理unknown selector,在这一步中,runtime系统会问它:可否把这条消息转给其他对象处理?该步骤对应的处理方法是forwardingTargetForSelector:,定义于<objc/NSObject.h>中:

- (id)forwardingTargetForSelector:(SEL)aSelector;

若当前receiver能找到备援对象,则将其返回,当然,备援对象必须能够响应aSelector,否则依然会抛出doesNotRecognizeSelector:异常;若找不到,则返回nil

-forwardingTargetForSelector:的使用逻辑非常简单,应用场景包括:

  • 实现多继承。Objective-C不允许多继承,基于-forwardingTargetForSelector:,可以通过组合的方式,模拟出多继承的某些特性。
  • 为协议遵循者提供默认实现。譬如某个协议定义了多个方法,有必要为这几个方法提供默认实现;具体做法是定义一个类(假设为Implement),用于实现这几个方法,然后override协议遵循者的-forwardingTargetForSelector:方法,将协议方法的receiver定位到Implement对象。

-forwardInvocation:

-forwardInvocation:要和-methodSignatureForSelector:配套使用,后者为NSMethodSignature对象,该对象携带selector的签名信息,包括参数类型、返回值类型和长度等。Runtime内部会基于NSMethodSignature实例构建一个NSInvocation对象,作为回调-forwardInvocation:的入参。

P.S: 知道NSMethodSignature对象携带selector的签名信息一般就够了,彻底了解它还得搞清楚type encodings,这东西挺没劲的,很少会和它打交道,更多信息参考这里这里

只要回调-methodSignatureForSelector:的返回值不为空,就会进入-forwardInvocation:方法,用户可以在此过程中修改invocation的target,将receiver定位到别处:

- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation setTarget:self.target]; // 让self.target成为消息的receiver
[invocation invoke];
}

值得一提的是,除了修改receiver,还可以修改入参,甚至是返回值。NSInvocation#invoke会触发receiver的selector的调用,如果不想调用怎么办?没怎么办,只要确保invocation的返回值(NSInvocation#setReturnValue:)的类型和长度一致即可。

Unknown selector触发的三个回调介绍完毕,简单总结一下。

就作用而言,+resolveInstanceMethod:主要用于为类动态增加实例方法;-forwardingTargetForSelector:用于将selector的receiver从self定位到别的target;这两个方法的使用都比较直接简单,不太能整出花样。-forwardInvocation:就不同了,在它身上可以动的手脚比较多,不光可以修改receiver,还可以篡改入参、返回值;当然,-forwardInvocation:的代价比较大一些,毕竟还会触发-methodSignatureForSelector:,构建NSMethodSignatureNSInvocation实例。

如果需要动态新增方法,可以在+resolveInstanceMethod:阶段完成;如果只是需要篡改receiver,在-forwardingTargetForSelector:阶段完成更省事儿;如果需要更高阶的玩法,或许真的只有-forwardInvocation:能满足需求。

一些应用场景

这一部分罗列一些简单而有意思的应用场景。

可以接受任何消息的NSNull

Objective-C里的nil是个好东西,向它发送任何消息都不会导致doesNotRecognizeSelector:异常;然而,它不能作为集合类型的有效元素,也不能作为nonnull类型的入参或返回值。NSNull是表达「空」这一概念的另一种类型,但它的局限在于不能像nil一样安全接受任何消息。然而,基于-forwardInvocation:可以比较容易完成这一目标,即让NSNull能安全接受任何消息,代码如下:

@implementation NSNull (Safe)
- (void)forwardInvocation:(NSInvocation *)invocation {
if ([self respondsToSelector:[invocation selector]]) {
[invocation invokeWithTarget:self];
}
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
NSMethodSignature *methodSignature = [[NSNull class] instanceMethodSignatureForSelector:selector];
if (!methodSignature) {
methodSignature = [NSMethodSignature signatureWithObjCTypes:"@:"]; // `@:`是随便定义的有效type encodings
}
return methodSignature;
}
@end

且不说这种设计的好坏,但它确实达到了目的:

  • -methodSignatureForSelector:的处理使得NSNull对象可以接受任何selector而不产生doesNotRecognizeSelector:异常
  • -forwardInvocation:的处理使得NSNull实例接受到unknown selector时,不做任何处理,即空操作

更多有趣的应用场景,有待补充…

本文参考