当你的才华撑不起你的野心时,你就应该静下来学习。 —— CJJ
什么时候会报unrecognized selector的异常?
- 当某个对象调用了一个本身没有实现的方法并且经过了“一系列流程”都没有找到处理的方法就会报这个异常。
如何避免?
- 首先,在
Objective-C
中,调用一个方法实际上内部是调用了发送消息的机制
[self testFunc];
相当于
objc_msgSend(self,@selector(testFunc));
- 这句代码所执行的流程大概是这样的
objc
给self
对象发送一个消息,在runtime
期间根据self
的isa
指针找到self
所属的类,然后找到该类的方法列表,寻找方法testFunc
- 若找到,则直接调用。
- 若找不到,将调用
+ (BOOL)resolveClassMethod:(SEL)sel;//处理类(+)方法
或者
+ (BOOL)resolveInstanceMethod:(SEL)sel;//处理实例(-)方法
查找一下有没有动态添加该方法
- 若找到,则直接调用
- 若没有找到,将进入“消息转发”流程。
- 首先调用
//只能转发给单个对象 - (id)forwardingTargetForSelector:(SEL)aSelector;
查找一下有没有可以转发的对象,并且该对象有该方法的实现
- 若有,则转发到对应的对象来执行该方法
- 若没有,则调用
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
获取选择子的方法签名
- 然后调用
//能够转发多个对象,但这一步消耗也是最大的 - (void)forwardInvocation:(NSInvocation *)anInvocation;
根据上一个方法获取的选择子,查找一下有没有可以转发的对象,并且该对象有该方法的实现
- 若有,则转发到对应的对象来执行该方法
- 若没有,则调用
- (void)doesNotRecognizeSelector:(SEL)aSelector;
- 抛出异常,使程序崩溃。
看完以上整个流程,大概可以理解消息传递以及因找不到方法而崩溃的原理了
那么我们就可以从流程中涉及的四个方法入手,来避免这个异常的发生
- 第一个救命稻草,动态添加方法
- (UIButton *)addMethodBtn{
if(!_addMethodBtn){
_addMethodBtn = [[UIButton alloc] initWithFrame:CGRectMake(100, 100, 200, 100)];
[_addMethodBtn setTitle:@"动态添加方法" forState:UIControlStateNormal];
[_addMethodBtn setTitleColor:[UIColor redColor] forState:UIControlStateNormal];
_addMethodBtn.backgroundColor = [UIColor yellowColor];
[_addMethodBtn addTarget:self action:@selector(instanceMethod:) forControlEvents:UIControlEventTouchUpInside];
}
return _addMethodBtn;
}
void instanceMethod(id self, SEL _cmd){
NSLog(@"unrecognized selector sent to instance,%@成功添加了实例方法%@",self,NSStringFromSelector(_cmd));
[ViewController performSelector:@selector(classMethod:)];
}
void classMethod(id self,SEL _cmd, int num){
NSLog(@"unrecognized selector sent to class,%@成功添加了类方法%@",self,NSStringFromSelector(_cmd));
}
#pragma mark - add method by runtime
+ (BOOL)resolveClassMethod:(SEL)sel{
if([NSStringFromSelector(sel) isEqualToString:@"classMethod:"]){
Class metaClass = objc_getMetaClass("ViewController");
class_addMethod(metaClass, sel, (IMP)classMethod, "v@:i");
return YES;
}
return [super resolveClassMethod:sel];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if([NSStringFromSelector(sel) isEqualToString:@"instanceMethod:"]){
class_addMethod([self class], sel, (IMP)instanceMethod, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
我们点击按钮时,就会向
self
发送一条信息寻找instanceMethod:
方法,由于该类没有实现此方法,此时就会进入resolveInstanceMethod
方法中,由于我们已经动态添加了方法,所以就可以直接调用,避免crash。
控制台打印
2020-05-28 13:30:56.217829+0800 CrashDemo[5168:146949] unrecognized selector sent to instance,<ViewController: 0x7fe303e02240>成功添加了实例方法instanceMehod:
在这里我顺便在
void instanceMethod(id self, SEL _cmd)
函数中调用了classMethod
,来测试一下找不到类方法的流程。
那么相应的就会进入(BOOL)resolveClassMethod:(SEL)sel
方法去处理,由于我们也已经动态添加了该类方法,所以也能处理,避免crash。
控制台打印
2020-05-28 13:42:04.599905+0800 CrashDemo[5268:151626] unrecognized selector sent to class,ViewController成功添加了类方法classMethod:
如果第一步还没有解决的话
- 第二个救命稻草,消息转发之
forwardingTargetForSelector
- (UIButton *)forwardingTargetBtn{
if(!_forwardingTargetBtn){
_forwardingTargetBtn = [[UIButton alloc] initWithFrame:CGRectMake(100, 250, 200, 100)];
[_forwardingTargetBtn setTitle:@"消息转发给单个对象" forState:UIControlStateNormal];
[_forwardingTargetBtn setTitleColor:[UIColor redColor] forState:UIControlStateNormal];
_forwardingTargetBtn.backgroundColor = [UIColor yellowColor];
[_forwardingTargetBtn addTarget:self action:@selector(forwardingTargetMethod) forControlEvents:UIControlEventTouchUpInside];
}
return _forwardingTargetBtn;
}
#pragma mark - message forwarding
- (id)forwardingTargetForSelector:(SEL)aSelector{
if([NSStringFromSelector(aSelector) isEqualToString:@"forwardingTargetMethod"]){
ForwardingTarget *obj = [ForwardingTarget new];
if([obj respondsToSelector:aSelector]){
return obj;
}
}
return [super forwardingTargetForSelector:aSelector];
}
由于我们在第一步并没有动态添加
forwardingTargetMethod
方法,所以会进入forwardingTargetForSelector
方法处理,在这里,我们创建了另一个类ForwardingTarget
,并且在此类中实现了forwardingTargetMethod
方法,那么我们就可以把消息转发到ForwardingTarget
,让他来处理,避免了crash
。
ForwardingTarget.m
#import "ForwardingTarget.h"
@implementation ForwardingTarget
- (void)forwardingTargetMethod{
NSLog(@"unrecognized selector sent to instance,%@成功接受了实例方法%@的转发",self,NSStringFromSelector(_cmd));
}
@end
控制台打印
2020-05-28 13:49:31.854193+0800 CrashDemo[5268:151626] unrecognized selector sent to instance,<ForwardingTarget: 0x6000017fc030>成功接受了实例方法forwardingTargetMethod的转发
如果第二步还没有解决的话
- 第三个救命稻草,消息转发之
forwardInvocation
- (UIButton *)forwardingInvocationBtn{
if(!_forwardingInvocationBtn){
_forwardingInvocationBtn = [[UIButton alloc] initWithFrame:CGRectMake(100, 400, 200, 100)];
[_forwardingInvocationBtn setTitle:@"消息转发给多个对象" forState:UIControlStateNormal];
[_forwardingInvocationBtn setTitleColor:[UIColor redColor] forState:UIControlStateNormal];
_forwardingInvocationBtn.backgroundColor = [UIColor yellowColor];
[_forwardingInvocationBtn addTarget:self action:@selector(forwardingInvocationMethod) forControlEvents:UIControlEventTouchUpInside];
}
return _forwardingInvocationBtn;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
if([NSStringFromSelector(aSelector) isEqualToString:@"forwardingInvocationMethod"]){
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
if([NSStringFromSelector(anInvocation.selector) isEqualToString:@"forwardingInvocationMethod"]){
ForwardInvocationOne *one = [ForwardInvocationOne new];
ForwardInvocationTwo *two = [ForwardInvocationTwo new];
[anInvocation invokeWithTarget:one];
[anInvocation invokeWithTarget:two];
}
}
由于我们在第二步中没有创建添加到能处理
forwardingInvocationMethod
方法的对象,那么就会进入methodSignatureForSelector
,获取该方法的选择子,然后调用forwardInvocation
去处理,与第二步的区别是,我们可以在这个方法中转发多个对象,我们创建了ForwardInvocationOne
和ForwardInvocationTwo
两个类,并且都有forwardingInvocationMethod
的对应实现,那么就会同时转发到这两个类中实现,避免crash
。(实际上一旦到了这个方法,你就可以在里面能做任何事情,你还可以不做任何处理,也不会crash
)
ForwardInvocationOne.m
#import "ForwardInvocationOne.h"
@implementation ForwardInvocationOne
- (void)forwardingInvocationMethod{
NSLog(@"unrecognized selector sent to instance,%@成功接受了实例方法%@的转发",self,NSStringFromSelector(_cmd));
}
@end
ForwardInvocationTwo.m
@implementation ForwardInvocationTwo
- (void)forwardingInvocationMethod{
NSLog(@"unrecognized selector sent to instance,%@成功接受了实例方法%@的转发",self,NSStringFromSelector(_cmd));
}
@end
控制台打印
2020-05-28 13:57:01.195675+0800 CrashDemo[5268:151626] unrecognized selector sent to instance,<ForwardInvocationOne: 0x6000017fc2b0>成功接受了实例方法forwardingInvocationMethod的转发
2020-05-28 13:57:01.195818+0800 CrashDemo[5268:151626] unrecognized selector sent to instance,<ForwardInvocationTwo: 0x6000017fc1f0>成功接受了实例方法forwardingInvocationMethod的转发
- 看到这里,那么我们就学会了如何去利用这三根救命稻草来避免线上的
unrecognized selector
崩溃啦
文尾献上demo
CJJCrashDemo