Crash拦截器 - 让unrecognized selector消失

在本文中,我们将了解到如下内容:

  1. 基础的消息转发流程
  2. unrecognized selector 拦截建议
  3. 快速转发(Fast Forwarding)拦截unrecognized selector
  4. 常规转发(Normal Forwarding)拦截unrecognized selector

前言

我们在第一天学习Objective-C这一门语言的时候,就被告知这是一门动态语言。
C这样的编译语言,在编译阶段就确定了所有函数的调用链,如果函数没有被实现,编译就根本不过了。而基于动态语言的特性,在编译期间,我们无法确认程序在运行时要调用哪一个函数,某一个未被实现的函数是否会在运行时被实现。
这样就可能会出现运行时发现调用的函数根本不存在的尴尬,这也就是我们收到unrecognized selector sent to XXX这样的崩溃的原因了(动态语言也有让人心累的地方,手动叹气)。

这篇文章要讨论的就是如果遇到了这种尴尬情况的时候,我们该如何避免我们最最最讨厌的崩溃(是的,所有的崩溃都是最最最让人讨厌的)。

消息转发流程

我们知道在我们调用某一个方法之后,最终调用的是objc_msgSend()这样一个方法,发送消息(selector)给消息接收者(receiver)。这个方法会根据OC的消息发送机制在receiver中查找selector。如果没有查找到,就会出现上述的运行时调用了未实现的函数的尴尬局面了。

不过为了缓解这种尴尬,我们还有机会来挣扎。这挣扎机会就是消息转发流程

消息转发流程包含以下3个步骤:

  1. 动态方法解析:resolveInstanceMethod:resolveClassMethod:
  2. 消息转发
    • 快速转发:forwardingTargetForSelector:
    • 常规转发:methodSignatureForSelector:forwardInvocation:

消息转发流程是以动态方法解析消息快速转发消息常规转发这样的顺序来执行的。如果其中任意一个步骤能使消息被执行,那么就不会出现unrecognized selector sent to XXX的崩溃

动态方法解析

resolveInstanceMethod:这个方法的作用是动态地为selector提供一个实例方法的实现。而resolveClassMethod:则是提供一个类方法的实现。

所以我们可以在这两个方法中,为对象添加方法的实现,再返回YES告诉已经为selector添加了实现。这样就会重新在对象上查找方法,找到我们新添加的方法后就直接调用。从而避免掉unrecognized selector sent to XXX

需要注意的是: 这两个方法会响应respondsToSelector:instancesRespondToSelector:

消息快速转发

forwardingTargetForSelector:的作用是将消息转发给其它对象去处理。
我们可以在这个方法中,返回一个对象,让这个对象来响应消息。

需要注意的是: 如果在这个方法中返回selfnil,则表示没有可响应的目标。

消息常规转发

forwardInvocation:的作用也是将消息转发给其它对象。不过与 消息快速转发 不同的是该方法需要手动的创建一个NSInvocation对象,并手动地将新消息发送给新的接收者。

很显然,这种方式会比 消息快速转发 付出更大的消耗。

如何选择拦截方案的建议

对于以上的三个步骤,我们该选择哪一个步骤来进行拦截呢?

  • 动态方法解析 - 不建议
    1. 这个方法会为类添加本身不存在的方法,绝大多数情况下,这个方法时冗余的。
    2. respondsToSelector:instancesRespondToSelector:这两个方法都会调用到resolveInstanceMethod:,那么在我们需要使用这两个方法进行判断的时候,就会出现我们不想看到的情况。
  • 消息快速转发 - 推荐
    会拦截掉已经通过消息常规转发实现的消息转发,但是可以通过判断避开对NSObject子类的消息常规转发的拦截。
  • 消息常规转发 - 推荐
    这一步不会对原有的消息转发机制产生影响,缺点是更大的性能开销。

快速转发拦截方案

我们可以创建一个例如:crashPreventor的类,在forwardingTargetForSelector:中为crashPreventor添加selector,最后返回crashPreventor的实例。从而让crashPreventor的实例响应这个selector。具体代码如下:


@implementation NSObject (SelectorPreventor)

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation"
- (id)forwardingTargetForSelector:(SEL)aSelector{
    Class rootClass = NSObject.class;
    Class currentClass = self.class;
    return [self.class actionForwardingTargetForSelector:aSelector rootClass:rootClass currentClass:currentClass];
}

+ (id)forwardingTargetForSelector:(SEL)aSelector {
    Class rootClass = objc_getMetaClass(class_getName(NSObject.class));
    Class currentClass = objc_getMetaClass(class_getName(self.class));
    return [self actionForwardingTargetForSelector:aSelector rootClass:rootClass currentClass:currentClass];
}

+ (id)actionForwardingTargetForSelector:(SEL)aSelector rootClass:(Class)rootClass currentClass:(Class)currentClass {
    // 过滤掉内部对象
    NSString *className = NSStringFromClass(currentClass);
    if ([className hasPrefix:@"_"]) {
        return nil;
    }

    SEL methodSignatureSelector = @selector(methodSignatureForSelector:);
    IMP rootMethodSignatureMethod = class_getMethodImplementation(rootClass, methodSignatureSelector);
    IMP currentMethodSignatureMethod = class_getMethodImplementation(currentClass, methodSignatureSelector);
    if (rootMethodSignatureMethod != currentMethodSignatureMethod) {
        return nil;
    }

    NSString * selectorName = NSStringFromSelector(aSelector);

    // 上报异常
    // unrecognized selector sent to class XXX
    // unrecognized selector sent to instance XXX
    NSLog(@"unrecognized selector crash:%@:%@", className, selectorName);

    // 创建crashPreventor类
    NSString *targetClassName = @"crashPreventor";
    Class cls = NSClassFromString(targetClassName);
    if (!cls) {
        // 如果要注册类,则必须要先判断class是否已经存在,否则会产生崩溃
        // 如果不注册类,则可以重复创建class
        cls = objc_allocateClassPair(NSObject.class, targetClassName.UTF8String, 0);
        objc_registerClassPair(cls);
    }

    // 如果类没有对应的方法,则动态添加一个
    if (!class_getInstanceMethod(cls, aSelector)) {
        Method method = class_getInstanceMethod(currentClass, @selector(crashPreventor));
        class_addMethod(cls, aSelector, method_getImplementation(method), method_getTypeEncoding(method));
    }

    return [cls new];
}

#pragma clang diagnostic pop

- (id)crashPreventor {
    return nil;
}

@end

这里有几个点需要提一下:

  1. - (id)forwardingTargetForSelector:(SEL)aSelector;+ (id)forwardingTargetForSelector:(SEL)aSelector;都要在NSObject的分类中重写。前者对应实例方法,后者对应类方法。
  2. 过滤掉一些系统内部对象,否则在启动的时候就会有一些奇怪的异常被捕获到。
  3. 我们需要判断当前类是否实现了methodSignatureForSelector:方法,如果实现了该方法,就认为当前类已经实现了自己的消息转发机制,我们不对其进行拦截。
  4. 细心的我们肯定有注意到,不管是类方法还是实例方法,我们都是向crashPreventor中添加实例方法。这是因为,我们的响应对象时crashPreventor实例,而selector不区分实例方法还是类方法。我们这么处理最终对方法执行来说不会有什么差别。

常规转发拦截方案

实现比较简单,我们直接上代码:

@implementation NSObject (SelectorPreventor)

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation"
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    return [NSMethodSignature signatureWithObjCTypes:"@"];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"forwardInvocation------");
}

+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    return [NSMethodSignature signatureWithObjCTypes:"@"];
}

+ (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"forwardInvocation------");
}

#pragma clang diagnostic pop

@end

同样的,类方法和实例方法我们都需要重写。
methodSignatureForSelector:中我们返回一个返回值为voidNSMethodSignature,在forwardInvocation:中我们不做任何事情。这样将性能消耗减到最小。

以上:我们可以选择其中一种方式来实现我们对unrecognized selector的拦截,跟unrecognized selector彻底说拜拜啦(手动微笑)。

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

推荐阅读更多精彩内容