runtime消息转发机制
在我们平时的开发中,当我们调用一个对象不存在的方法的时候,会提示如下错误
unrecognized selector sent to instance 0x1c5035312
可见,调用没有的方法实现,程序会在运行时挂掉并抛出 unrecognized selector sent to … 的异常。但在异常抛出前,Objective-C 的运行时会给你三次拯救程序的机会,原理图如下:
从全局来看,消息转发机制共分为3大步骤:
1.Method resolution 动态方法解析阶段
2.Fast forwarding 快速转发阶段
3.Normal forwarding 常规转发阶段
那么如果想要不抛出unrecognized selector 的报错,也就需要从这3步里面来做补救了,我们一步一步来看如何在这3个阶段来进行补救。
第一步:Method resolution 动态方法解析
如果调用了对象方法首先会进行+(BOOL)resolveInstanceMethod:(SEL)sel
判断
如果调用了类方法 首先会进行 +(BOOL)resolveClassMethod:(SEL)sel
判断
两个方法都为类方法,因为是向接收者所属的类进行请求,如果YES则能接受消息 NO不能接受消息 进入第二步
[self performSelector:@selector(testFunction:) withObject:@"test"];
[[ViewController class] performSelector:@selector(testClassFunction:) withObject:@"test"];
void dynamicAdditionMethodIMP(id self, SEL _cmd) {
NSLog(@"dynamicAdditionMethodIMP");
}
+ (BOOL)resolveInstanceMethod:(SEL)name {
NSLog(@"resolveInstanceMethod: %@", NSStringFromSelector(name));
if (name == @selector(testFunction:)) {
class_addMethod([self class], name, (IMP)dynamicAdditionMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:name];
}
+ (BOOL)resolveClassMethod:(SEL)name {
NSLog(@"resolveClassMethod %@", NSStringFromSelector(name));
if (name == @selector(testClassFunction:)) {
class_addMethod(objc_getMetaClass(class_getName([self class])), name, (IMP)dynamicAdditionMethodIMP, "v@:");
return YES;
}
return [super resolveClassMethod:name];
}
运行结果如下,不会再抛出异常
2019-04-16 15:02:19.323931+0800 MsgForwardDemo[6288:476940] resolveInstanceMethod: testFunction:
2019-04-16 15:02:19.324038+0800 MsgForwardDemo[6288:476940] dynamicAdditionMethodIMP
2019-04-16 15:02:19.324156+0800 MsgForwardDemo[6288:476940] resolveClassMethod testClassFunction:
2019-04-16 15:02:19.324264+0800 MsgForwardDemo[6288:476940] dynamicAdditionMethodIMP
注意:
1、 签名符号含义:
* 代表 char *
char BOOL 代表 c
: 代表 SEL
^type 代表 type *
@ 代表 NSObject * 或 id
^@ 代表 NSError **
# 代表 NSObject
v 代表 void
2、类方法需要添加到元类里面
OC中所有的类本质上来说都是对象,对象的isa指向本类,类的isa指向元类,元类的isa指向根元类,根元类的isa指向自己,这样的话就形成了一个闭环。
第二步:Fast forwarding 快速消息转发
如果在上一步的2个方法内返回NO,代表不能接受消息 则进入第二步,我们先把上面方法内的处理方案注释掉,让消息转发进入第二步,这个方法是转发SEL去其他可以响应该方法的对象。
我们新创建一个NewViewController类,里面声明和实现testFunction:方法,用来当作备用响应者。
- (void) testFunction:(NSString *)string {
NSLog(@"NewViewController method testFunction called");
}
-(id)forwardingTargetForSelector:(SEL)aSelector{
if ([NSStringFromSelector(aSelector) isEqualToString:@"testFunction:"]) {
return [NewViewController new];
}
return [super forwardingTargetForSelector:aSelector];
}
运行结果如下
2019-04-16 15:51:38.786728+0800 MsgForwardDemo[7014:546250] NewViewController method testFunction called
第三部:Normal forwarding 常规消息转发
如果第2步返回self或者nil,则说明没有可以响应的目标 则进入第三步。
第三步的消息转发机制本质上跟第二步是一样的都是切换接受消息的对象,但是第三步切换响应目标更复杂一些,第二步里面只需返回一个可以响应的对象就可以了,第三步还需要手动将响应方法切换给备用响应对象。
有两个步骤:
(1)-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
在第(1)步中,返回SEL方法的签名,返回的签名是根据方法的参数来封装的。
1.手动创建签名,但是尽量少使用,因为容易创建错误
根据OBJC的编码类别进行编写后面的char (但是容易写错误,所以建议使用下面的方法)
NSMethodSignature *sign = [NSMethodSignature signatureWithObjCTypes:"v@:"];
//写法例子
//例子"v@:@"
//v@:@ v:返回值类型void;@ id类型,执行sel的对象;:SEL;@参数
//例子"@@:"
//@:返回值类型id;@id类型,执行sel的对象;:SEL
2.自动创建签名
NewViewController * newVC = [NewViewController new];
NSMethodSignature * sign = [newVC methodSignatureForSelector:aSelector];
使用对象本身的methodSignatureForSelector自动获取该SEL对应类别的签名
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
//如果返回为nil则进行手动创建签名
if ([super methodSignatureForSelector:aSelector]==nil) {
NSMethodSignature * sign = [NSMethodSignature signatureWithObjCTypes:"v@:"];
return sign;
}
return [super methodSignatureForSelector:aSelector];
}
(2)-(void)forwardInvocation:(NSInvocation *)anInvocation
上方的第(1)步中如果调用返回有签名 则进入消息转发最后一步
-(void)forwardInvocation:(NSInvocation *)anInvocation{
//创建备用对象
NewViewController * newVC = [NewViewController new];
SEL sel = [anInvocation selector];
//判断备用对象是否可以响应传递进来等待响应的SEL
if ([newVC respondsToSelector:sel]) {
[anInvocation invokeWithTarget:newVC];
}else{
// 如果备用对象不能响应 则抛出异常
[self doesNotRecognizeSelector:sel];
}
}
运行结果如下
2019-04-16 16:11:00.219757+0800 MsgForwardDemo[7267:576487] NewViewController method testFunction called
总的来看,resolvedInstanceMethod 适合给类/对象动态添加一个相应的实现,forwardingTargetForSelector 适合将消息转发给其他对象处理,相对而言,forwardInvocation 是里面最灵活,可以处理各种复杂情况。
在三个步骤的每一步,消息接受者都还有机会去处理消息。同时,越往后面处理代价越高,最好的情况是在第一步就处理消息,这样runtime会在处理完后缓存结果,下回再发送同样消息的时候,可以提高处理效率。第二步转移消息的接受者也比进入转发流程的代价要小,如果到最后一步forwardInvocation的话,就需要处理完整的NSInvocation对象了。