iOS RunTime Advance 03 (消息转发)

消息转发

前文介绍了进行一次发送消息会在相关的类对象中搜索方法列表,如果找不到则会沿着继承树向上一直搜索直到继承树根部(通常为NSObject),如果还是找不到并且消息转发都失败了就回执行 doesNotRecognizeSelector: 方法报 unrecognized selector 错。

但在抛出异常之前,还有 三次机会 按以下顺序让你拯救程序:

  1. Method Resolution 动态方法解析
  2. Fast Forwarding 备用接收者
  3. Normal Forwarding 完整消息转发

消息转发流程简图:

动态方法解析 Method Resolution

首先,OC运行时会调用 +resolveInstanceMethod:或者 +resolveClassMethod:,让你有机会提供一个函数实现。如果你添加了函数并返回YES, 那运行时系统就会重新启动一次消息发送的过程。

实现一个动态方法解析的例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@interface Message : NSObject

- (void)sendMessage:(NSString *)word;

@end

@implementation Message

- (void)sendMessage:(NSString *)word
{
NSLog(@"normal way : send message = %@", word);
}

@end

如果创建Message对象并调用sendMessage:方法:

1
2
Message *message = [Message new];
[message sendMessage:@"App Dev Club"];

控制台会打印以下信息:

1
normal way : send message = App Dev Club

但现在我将原来sendMessage:方法实现给注释掉,覆盖resolveInstanceMethod:方法:

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
#pragma mark - Method Resolution

/// override resolveInstanceMethod or resolveClassMethod for changing sendMessage method implementation
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
//形式1:
if (sel == @selector(sendMessage:)) {
class_addMethod([self class], sel, imp_implementationWithBlock(^(id self, NSString *word) {
NSLog(@"method resolution way : send message = %@", word);
}), "v@*");

return YES;
}

//形式2:
if (sel == @selector(foo:)) {//如果是执行foo函数,就动态解析,指定新的IMP
class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
return YES;
}

return [super resolveInstanceMethod:sel];
}

void forwardMethod(id obj, SEL _cmd) {
NSLog(@"Doing foo");//新函数
}

控制台就会打印以下信息:

1
method resolution way : send message = App Dev Club

注意到上面代码有这样一个字符串"v@*"("v@:"),它表示方法的参数和返回值,详情参考 Type Encodings

可以看到虽然没有实现sendMessage:这个函数,但是我们通过class_addMethod动态添加函数,并执行这个函数的IMP。从结果看,成功实现了。
如果resolve方法返回 NO ,运行时就会移到下一步:forwardingTargetForSelector

备用接收者 Fast Forwarding

如果目标对象实现- forwardingTargetForSelector:方法,系统就会在运行时调用这个方法,只要这个方法返回的不是nilself,也会重启消息发送的过程,把这消息转发给其他对象来处理。否则,就会继续Normal Fowarding
继续上面Message类的例子,将sendMessage:resolveInstanceMethod:方法注释掉,然后添加forwardingTargetForSelector:方法的实现:

1
2
3
4
5
6
7
8
9
#pragma mark - Fast Forwarding
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if (aSelector == @selector(sendMessage:)) {
return [MessageForwarding new];/返回一个新对象,让新对象接收这个消息
}

return [super forwardingTargetForSelector:aSelector];
}

此时还缺一个转发消息的类MessageForwarding,这个类的设计与实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@interface MessageForwarding : NSObject

- (void)sendMessage:(NSString *)word;

@end

@implementation MessageForwarding

- (void)sendMessage:(NSString *)word
{
NSLog(@"fast forwarding way : send message = %@", word);
}

@end

此时,控制台会打印以下信息:

1
fast forwarding way : send message = App Dev Club

这里叫Fast,是因为这一步不会创建NSInvocation对象,但Normal Forwarding会创建它,所以更快点。

完整消息转发 Normal Forwarding

如果没有使用Fast Forwarding来消息转发,则唯一能做的就是启用完整的消息转发机制了.

首先它会发送-methodSignatureForSelector:消息获得函数的参数返回值类型
如果-methodSignatureForSelector:返回 nil ,Runtime则会发出 -doesNotRecognizeSelector:消息,程序这时也就crash掉了,并抛出 unrecognized selector sent to instance 异常信息。
如果返回了一个函数签名,Runtime就会创建一个NSInvocation对象并发送 -forwardInvocation:消息给目标对象。

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#pragma mark - Normal Forwarding
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector];

if (!methodSignature) {
methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:*"];
}

return methodSignature;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
MessageForwarding *messageForwarding = [MessageForwarding new];

if ([messageForwarding respondsToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:messageForwarding];
}
}

三种方法的选择

Runtime提供三种方式来将原来的方法实现代替掉,那该怎样选择它们呢?

  • Method Resolution:由于Method Resolution不能像消息转发那样可以交给其他对象来处理,所以只适用于在原来的类中代替掉。

  • Fast Forwarding:它可以将消息处理转发给其他对象,使用范围更广,不只是限于原来的对象。

  • Normal Forwarding:它跟Fast Forwarding一样可以消息转发,但它能通过NSInvocation对象获取更多消息发送的信息,例如:target、selector、arguments和返回值等信息。

坚持原创技术分享,您的支持将鼓励我继续创作!