消息转发机制

先举个🌰吧。

1
2
3
4
5
6
7
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSString *num = [NSNumber numberWithInt:12];
NSLog(@"i = %ld", num.length);
}
@end

上述代码,在实际运行中会发生闪退。理由正是 unrecognized selector sent to instance xxxx 。

1

其实这个问题的根源就是实例找不到对应的方法,所以崩了。有什么方法可以进行规避吗?这个的话可以考虑用消息转发机制。在《Effetive Objective-C 2.0》一书中有这样的描述。

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

按照书中的讲解,可以分成两个阶段。一是动态方法解析,二是完整的消息转发机制。并且,按照先后次序,越早进行补救,代价也就越小。

动态方法解析过程,主要是涉及到以下方法。

1
2
3
4
// 针对实例方法
+ (BOOL)resolveInstanceMethod:(SEL)sel
// 针对类方法
+ (BOOL)resolveClassMethod:(SEL)sel

比如就实例方法而言,当🌰中的 NSNumber 找不到对应的 length 方法时,那么会先到 +(BOOL)resolveInstanceMethod 方法中寻找。此时,我们可以进行一些判断,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@implementation NSNumber (Extension)
NSUInteger length(id self) {
NSString *str = [NSString stringWithFormat:@"%@",self];
return str.length;
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(length)) {
class_addMethod(self, sel, (IMP)length, "I@:@");
return YES;
}
return [super respondsToSelector:sel];
}
@end

事先实现好 length 的方法,之后用 class_addMethod 方法将该方法添加进去。如果这一步未能处理好,那么则会使用- (id)forwardingTargetForSelector:(SEL)aSelector方法来其转发给其他的处理类。

1
2
3
4
5
6
- (id)forwardingTargetForSelector:(SEL)aSelector {
if ([NSString instancesRespondToSelector:aSelector]) {
return [NSString stringWithFormat:@"%@",self];
}
return [super forwardingTargetForSelector:aSelector];
}

如果转发给其他的处理类还是处理不了的话,那么才会进入完整的消息转发。这是则需要使用到 NSMethodSignature 和 NSInvocation 来处理。并且对应的两个方法都是存在。按照 NSNumber 去执行 length 方法的话,其实在 methodSignatureForselector 这个方法中处理时,可以通过 NSMethodSignature 来对 length 方法进行签名,也可以直接利用 NSString 来返回 lenght selector 的方法签名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *signat = [super methodSignatureForSelector:aSelector];
if (!signat) {
// 有两种方法
// 1. 生成对应的可以响应该 selector 的方法签名
if ([NSString instancesRespondToSelector:aSelector]) {
signat = [[NSString alloc] methodSignatureForSelector:aSelector];
}
// 2. 返回该 selector 的一些信息,比如 返回值,参数类型等
if (aSelector == @selector(length)) {
signat = [NSMethodSignature signatureWithObjCTypes: "I@:"];
}
}
return signat;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
if (anInvocation.selector == @selector(length)) {
NSString *newObj = [NSString stringWithFormat:@"%@",self];
[anInvocation invokeWithTarget:newObj];
} else {
[super forwardInvocation:anInvocation];
}
}

对 unrecognized selector sent to instance xxxx 的规避大概就这些。如果只是按照例子中,NSNumber perform length 这种情况的话,其实很多时候都用不到这么复杂的消息转发机制。但是实际上开发中,我们面对的并不仅仅是这种情况,而是远比这个更复杂的。那么是不是可以考虑返回一个🈳️的方法,打印一些提示,来保证 app 不会闪退?

比如可以给对应的类扩展一个 doNothing 的方法。然后执行进行消息转发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@implementation NSNumber (Extension)
- (void)doNothing {
NSLog(@"not this selector");
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *signat = [super methodSignatureForSelector:aSelector];
if (signat) return signat;
signat = [ instanceMethodSignatureForSelector:@selector(doNothing)];
return signat;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
if (![self respondsToSelector:anInvocation.selector]) {
[anInvocation setTarget:self];
[anInvocation setSelector:@selector(doNothing)];
[anInvocation invoke];
} else {
[super forwardInvocation:anInvocation];
}
}
@end

大致上就是这样的一种实现吧。

参考

  1. Message Forwarding