iOS-Crash之unrecognized selector(runtime消息转发)

当你的才华撑不起你的野心时,你就应该静下来学习。 —— CJJ

什么时候会报unrecognized selector的异常?

  • 当某个对象调用了一个本身没有实现的方法并且经过了“一系列流程”都没有找到处理的方法就会报这个异常。

如何避免?

  • 首先,在Objective-C中,调用一个方法实际上内部是调用了发送消息的机制
[self testFunc];

相当于

objc_msgSend(self,@selector(testFunc));
  • 这句代码所执行的流程大概是这样的
  • objcself对象发送一个消息,在runtime期间根据selfisa指针找到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去处理,与第二步的区别是,我们可以在这个方法中转发多个对象,我们创建了ForwardInvocationOneForwardInvocationTwo两个类,并且都有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


参考文章
iOS理解Objective-C中消息转发机制附Demo
Objective-C 消息发送与转发机制原理

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268