前言
如果在动态解析阶段不做任何处理的话,我们调用一个未实现的方法会crash,下面来分析一下,crash之前系统还做了什么?
一、探索消息转发
1. instrumentObjcMessageSends打开log开关
#import <Foundation/Foundation.h>
#import "DZStudent.h"
extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
@autoreleasepool {
DZStudent *student = [DZStudent alloc] ;
instrumentObjcMessageSends(true);
[student saySomething];
instrumentObjcMessageSends(false);
}
return 0;
}
extern void instrumentObjcMessageSends(BOOL flag)
:是苹果的私有API,我们可以控制log开关,打印日志信息。
日志文件位置:/tmp/msgSend-xxx
2. 查看日志文件
我们可以发现,方法在调用报失败doesNotRecognizeSelector
之前的调用顺序:resolveInstanceMethod -> forwardingTargetForSelector -> methodSignatureForSelector -> doesNotRecognizeSelector。
resolveInstanceMethod
是动态方法决议,我们上一文已经做了分析,本文只针对后边的方法进行源码分析。
二、快速转发
1. forwardingTargetForSelector分析
我们全局搜索后,发现这个方法是NSObject
中实现的方法,只做了返回nil
的操作:
+ (id)forwardingTargetForSelector:(SEL)sel {
return nil;
}
我们可以结合方法介绍或者官方文档来进行分析:
2. 方法说明(看discussion):
- 该方法的目的就是不能处理方法的时候,交给另外一个对象来执行,但是不能返回
self
,否则会一直找不到陷入死循环。- 该方法效率很高,如果不实现或者返回
nil
,会走到相对效率低的forwardInvocation:
方法进行处理。- 所以我们称
forwardingTargetForSelector
为快速转发,forwardInvocation
为慢速转发。- 被转发的消息接收者,参数和返回值等需要和原方法相同。
3. 方法使用
当访问
DZStudent
未实现的saySomething
方法时,可以使用- (id)forwardingTargetForSelector:(SEL)aSelector
进行方法转发,用DZTeacher
这个实现saySomething
方法的对象来接收,具体实现代码如下:
main.m
DZStudent *student = [DZStudent alloc];
instrumentObjcMessageSends(true);
[student saySomething];
instrumentObjcMessageSends(false);
DZStudent.m
// 消息转发流程
- (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"%s",__func__);
if (aSelector == @selector(saySomething)) {
return [DZTeacher alloc];
}
return [super forwardingTargetForSelector:aSelector];
}
DZTeacher.m
@implementation DZTeacher
- (void)saySomething{
NSLog(@"%s",__func__);
}
@end
打印结果
// 失败打印
2020-06-17 15:58:28.646428+0800 008-方法查找-消息转发[17172:5671635] -[DZStudent saySomething]: unrecognized selector sent to instance 0x101805980
2020-06-17 15:58:28.658327+0800 008-方法查找-消息转发[17172:5671635] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[DZStudent saySomething]: unrecognized selector sent to instance 0x101805980'
// 成功打印
2020-06-17 14:58:14.506074+0800 008-方法查找-消息转发[10077:5556271] -[DZStudent forwardingTargetForSelector:] -- saySomething
2020-06-17 14:58:14.507704+0800 008-方法查找-消息转发[10077:5556271] -[DZTeacher saySomething]
通俗点讲,这个方法的作用就是,自己的活自己干不了,就交给能干活的人去干。
三、慢速转发
当快速转发流程也没有实现,或者返回nil
,就进入慢速转发流程。
1. methodSignatureForSelector
同样在源码中全局搜索之后,我们发现这个方法也是NSObject
中实现的方法:
// Replaced by CF (returns an NSMethodSignature)
+ (NSMethodSignature *)instanceMethodSignatureForSelector:(SEL)sel {
_objc_fatal("+[NSObject instanceMethodSignatureForSelector:] "
"not available without CoreFoundation");
}
// Replaced by CF (returns an NSMethodSignature)
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
_objc_fatal("+[NSObject methodSignatureForSelector:] "
"not available without CoreFoundation");
}
// Replaced by CF (returns an NSMethodSignature)
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
_objc_fatal("-[NSObject methodSignatureForSelector:] "
"not available without CoreFoundation");
}
官方文档:
方法说明:
该方法用于协议的实现,如果有对象未能直接实现的消息,则重写此方法返回适当的方法签名。然后将签名对象作为参数传给
forwardInvocation
方法,在forwardInvocation
里边将消息给能处理该消息的对象,避免最后调用didNotRecognizeSelector
方法导致崩溃。
下来我们继续了解 forwardInvocation 方法:
2. forwardInvocation
同样是在源码中全局搜索之后,我们发现这个方法也是NSObject中实现的:
+ (void)forwardInvocation:(NSInvocation *)invocation {
[self doesNotRecognizeSelector:(invocation ? [invocation selector] : 0)];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
[self doesNotRecognizeSelector:(invocation ? [invocation selector] : 0)];
}
官方文档:
forwardInvocation
和methodSignatureForSelector
必须是同时重写。并且该方法可以自由指派多个对象接受该消息。
3. doesNotRecognizeSelector
// Replaced by CF (throws an NSException)
+ (void)doesNotRecognizeSelector:(SEL)sel {
_objc_fatal("+[%s %s]: unrecognized selector sent to instance %p",
class_getName(self), sel_getName(sel), self);
}
// Replaced by CF (throws an NSException)
- (void)doesNotRecognizeSelector:(SEL)sel {
_objc_fatal("-[%s %s]: unrecognized selector sent to instance %p",
object_getClassName(self), sel_getName(sel), self);
}
我们可以发现,最终是doesNotRecognizeSelector
方法抛出的异常,所以我们可以重写forwardInvocation
方法,这样不执行父类的方法,程序就不会崩溃了。
4. 方法使用
在forwardInvocation
方法中,我们可以把这个方法看成是一个未知方法收集箱,在这里可以随意选择你可以处理的方法,进行归类集中处理。
main.m
#import <Foundation/Foundation.h>
#import "DZStudent.h"
extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
@autoreleasepool {
DZStudent *student = [DZStudent alloc];
instrumentObjcMessageSends(true);
[student saySomething];
instrumentObjcMessageSends(false);
}
return 0;
}
DZTeacher.m
@implementation DZTeacher
- (void)saySomething{
NSLog(@"%s",__func__);
}
@end
DZStudent.m
备注:关于方法签名串"v@:"可以参考官方文档:方法签名Type Encodings
// 返回一个方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSLog(@"%s -- %@",__func__,NSStringFromSelector(aSelector));
if (aSelector == @selector(saySomething)) { // v @ :
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"%s ",__func__);
SEL aSelector = [anInvocation selector];
if ([[DZTeacher alloc] respondsToSelector:aSelector])
[anInvocation invokeWithTarget:[DZTeacher alloc]];
else
[super forwardInvocation:anInvocation];
}
// 打印
2020-06-17 17:17:08.148187+0800 008-方法查找-消息转发[24033:5801203] -[DZStudent methodSignatureForSelector:] -- saySomething
2020-06-17 17:17:08.149424+0800 008-方法查找-消息转发[24033:5801203] -[DZStudent forwardInvocation:]
当然此处转发方法也可以什么都不做处理,也仅仅是转发不出去而已,并不会崩溃。
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"%s ",__func__);
}
// 打印
2020-06-17 18:24:21.692113+0800 008-方法查找-消息转发[32243:5925309] -[DZStudent methodSignatureForSelector:] -- saySomething
2020-06-17 18:24:21.699464+0800 008-方法查找-消息转发[32243:5925309] -[DZStudent forwardInvocation:]
四、总结
- 当动态方法决议也没有做处理时,就会进入快速转发(
forwardingTargetForSelector
)阶段。 - 如果快速转发也没有做处理,会继续到慢速转发(
forwardInvocation
)阶段。 - 即使
forwardInvocation
中不实现后续方法也不会崩溃。 -
forwardInvocation
和forwardingTargetForSelector
类似,都可以将A类的方法转发的B类的实现中去,但是forwardInvocation
的优点是更加灵活,forwardingTargetForSelector
只能转发发到固定的一个对象。而forwardInvocation
可以转发的多个对象中去,甚至不做处理,也仅仅是转发不出去而已,并不会崩溃。
消息的流转及对应的作用如下所示:
流转:
消息发送 -> 消息查找 -> 动态方法决议 -> 快速转发 -> 慢速转发
作用:
像某个对象或类发送消息 -> 自己有没有处理 -> 自己有没有特殊处理(动态方法决议) -> 指定的人有没有处理 -> 爱谁处理谁处理