消息转发
经过了快速查找,慢速查找,动态方法解析之后,仍然没有找到SEL
对应的IMP
,此时我们在抛出异常信息的信息的时候,查看一下当前堆栈情况
frame #9: 0x00000001002e8c7e libobjc.A.dylib`objc_exception_throw(obj="-[HQPerson say666]: unrecognized selector sent to instance 0x100665a40") at objc-exception.mm:591:5
frame #10: 0x00007fff314de936 CoreFoundation`-[NSObject(NSObject) doesNotRecognizeSelector:] + 132
frame #11: 0x00007fff313c3ec0 CoreFoundation`___forwarding___ + 1427
frame #12: 0x00007fff313c3898 CoreFoundation`__forwarding_prep_0___ + 120
frame #13: 0x0000000100003b1e HQObjc`main(argc=1, argv=0x00007ffeefbff4f0) at main.m:154:9 [opt]
frame #14: 0x00007fff6b547cc9 libdyld.dylib`start + 1
从上面的堆栈信息中可以看出,在doesNotRecognizeSelector
函数(该函数为抛出未找到selector的异常信息)之前,有两步CoreFoundation
的操作,但___forwarding___
和__forwarding_prep_0___
.CoreFoundation
是不开源的,那如何分析这两个操作呢?这可以通过使用hopper工具进行反编译查看这两个函数的流程.本章忽略如何使用hopper
工具.直接给出结果:
-
__forwarding_prep_0___
函数中调用___forwarding___
; - 在
___forwarding___
的伪代码中,首先是查看当前类是否实现forwardingTargetForSelector
方法(快速消息转发).- 若实现该方法,进执行该方法.
- 若未实现该方法,则查看是否实现
methodSignatureForSelector
方法(慢速消息转发).- 若
methodSignatureForSelector
方法未实现,则直接报错. - 若
methodSignatureForSelector
方法有实现,则判断该方法的返回值.- 返回值为nil,直接报错.
- 返回值不为nil,在forwardInvocation方法中对invocation进行处理.
- 若
快速消息转发
forwardingTargetForSelector
:当进行快速查找,慢速查找,动态方法解析
之后,仍然没有查到SEL
对应的IMP
,这个时候,苹果给我们提供了第二次挽救的机会
,即快速消息转发
.
快速消息转发的中心思想是,既然在当前消息接收者中找不到SEL
,那通过forwardingTargetForSelector
函数,将当前的消息接收者可以转发给其它能处理该消息的类.
注意:forwardingTargetForSelector
方法分为实例方法
和类方法
,这两个方法在NSObject
中都有实现,只是返回值为nil.
接下来尝试在第二次挽救机会中对HQPerosn的实例方法say666进行挽救.
- 由于快速消息转发是将消息转发给能处理该消息的其它类,因此需要定义一个其它类,并实现say666方法.
@interface HQHuman : NSObject
-(void)say666;
@end
@implementation HQHuman
-(void)say666{
NSLog(@"%s", __func__);
}
@end
- 在HQPerson类中,由于是对实例方法的挽救,因此需要实现
forwardingTargetForSelector
实例方法.
- (id)forwardingTargetForSelector:(SEL)aSelector{
if(aSelector == NSSelectorFromString(@"say666")){
return [HQHuman alloc];
}
return nil;
}
- 尝试
向HQPerson对象发送say666消息
,查看结果
2021-02-01 15:33:18.311088+0800 HQObjc[37844:2499110] Hello, World!
2021-02-01 15:33:21.403814+0800 HQObjc[37844:2499110] -[HQHuman say666]
2021-02-01 15:33:22.842117+0800 HQObjc[37844:2499110] end~
Program ended with exit code: 0
由结果可以看到,HQPerson类
虽然没有实现say666的实例方法
,但经过快速消息转发
后,将say666消息
的接收者
修改成HQHuman
了.最终由HQHuman完成消息的执行
.
慢速消息转发
要了解慢速消息转发,需要先了解NSInvocation
.
NSInvocation
是命令模式
的一种传统实现,它把一个目标、一个选择器、一个方法签名和所有的参数
都塞进一个对象
里,这个对象可以先存储起来,以备将来调用.
NSInvocation
还包含一个方法签名(NSMethodSignature)
,它封装了一个方法的返回类型
和参数类型
,记住它不包括方法名称,只有返回类型和参数类型
。可以通过以下方法,手动的创建一个方法签名:
NSMethodSignature *sig = [NSMethodSignature signatureWithObjCTypes:"v@:"];
最后,NSInvocation
还包含了所有的参数
.
回到慢速消息转发.
当经过第二次挽救时,仍然未找到该SEL
对应的IMP
,此时苹果最后给了一次机会
.即先调用methodSignatureForSelector
,查看当前是否有返回方法签名
.
若当前未返回方法签名,即返回nil,则报错.
若返回的方法签名不为nil,则创建一个NSInvocation
,并传递给forwardInvocation
.在前面对NSInvocation
的描述中知道,有了NSInvocation
对象,就能完成消息的执行.
注意:methodSignatureForSelector
方法和forwardInvocation
方法分为实例方法
和类方法
,这两个方法在NSObject
中都有实现.
接下来尝试在最后一次挽救机会中对HQPerosn的实例方法say666进行挽救.
- 通过
methodSignatureForSelector
方法,返回say666实例方法
的方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
- 经过上一步之后,
CoreFoundation
会创建一个NSInvocation
,并NSInvocation
的methodSignature
设置为上一步的返回值.接下来,需要在forwardInvocation
中指定接收消息的接收者,传递的参数等信息.由于本案例没有参数,因此,只设定消息接收者即可.
- (void)forwardInvocation:(NSInvocation *)anInvocation{
[anInvocation invokeWithTarget:[[HQHuman alloc] init]];
}
- 尝试
向HQPerson对象发送say666消息
,查看结果
2021-02-01 16:29:06.990005+0800 HQObjc[38332:2540396] Hello, World!
2021-02-01 16:29:12.188971+0800 HQObjc[38332:2540396] -[HQHuman say666]
2021-02-01 16:29:13.685739+0800 HQObjc[38332:2540396] end~
Program ended with exit code: 0
由结果同样可以看到,HQPerson类
虽然没有实现say666的实例方法
,但经过慢速消息转发
后,将say666消息
的接收者
修改成HQHuman
了.最终由HQHuman完成消息的执行
.
网友评论