iOS防崩溃

利用Objective-C语言的动态特性,采用AOP(Aspect Oriented Programming) 面向切面编程的设计思想,做到无痕植入。能够自动在app运行时实时捕获导致app崩溃的破环因子,然后通过特定的技术手段去化解这些破坏因子,使app免于崩溃,照样可以继续正常运行,为app的持续运转保驾护航。当然我们不可能强大到把所有类型的crash都处理掉,但是我们会对一些高频的crash进行一一的处理,我们的目的就是降低crash率。

我们常见的crash有哪些呢?

  • unrecognized selector crash (没找到对应的函数)
  • KVO crash :(KVO的被观察者dealloc时仍然注册着KVO导致的crash,添加KVO重复添加观察者或重复移除观察者)
  • NSNotification crash:(当一个对象添加了notification之后,如果dealloc的时候,仍然持有notification)
  • NSTimer类型crash:(需要在合适的时机invalidate 定时器,否则就会由于定时器timer强引用target的关系导致 target不能被释放,造成内存泄露,甚至在定时任务触发时导致crash)
  • Container类型crash:(数组,字典,常见的越界,插入,nil)
  • 野指针类型的crash
  • 非主线程刷UI类型:(在非主线程刷UI将会导致app运行crash)

1、Unrecognized Selector类型crash防护

unrecognized selector类型的crash在app众多的crash类型中占着比较大的成分,通常是因为一个对象调用了一个不属于它方法的方法导致的。

方法调用流程:
  1. 首先,在相应操作的对象中的缓存方法列表中找调用的方法,如果找到,转向相应的函数
  2. 如果没找到在对象的类方法列表中找调用的方法,如果找到,转向相应的实现执行
  3. 如果没有找到,去父类指针指向的对象中执行1,2
  4. 以此类推,如果一直到根类还没有找到,转向拦截调用,走消息转发
  5. 如果没有重写拦截调用的方法,程序报错。
拦截调用:

拦截调用就是,在找不到调用的方法程序崩溃之前,你有机会通过重写NSObject的四个方法来处理:

+ (BOOL)resolveClassMethod:(SEL)sel; //动态类方法决议机制,决议类方法
+ (BOOL)resolveInstanceMethod:(SEL)sel;//动态的对象方法决议,决议对象方法

//后两个方法需要转发到其他的类处理
- (id)forwardingTargetForSelector:(SEL)aSelector;//转发给其他的一个对象处理函数
- (void)forwardInvocation:(NSInvocation *)anInvocation;//灵活的将目标函数以其他形式执行

拦截调用的整个流程即Objective-C的消息转发机制,我们不难发现,runtime提供了3种方式去补救:

  1. 调用resolveInstanceMethod给个机会让类添加这个实现这个函数
  2. 调用forwardingTargetForSelector让别的对象去执行这个函数
  3. 调用forwardInvocation(函数执行器)灵活的将目标函数以及其他形式执行。

如果都不行,系统才会调用doesNotRecognizeSelector抛出异常。

既然可以补救,我们完全也可以利用消息转发机制来做文章,但是我们选择哪一步比较合适呢?

  1. resolveInstanceMethod需要在类的本身动态的添加它本身不存在的方法,这些方法对于该类本身来说是冗余的
  2. forwardInvocation可以通过NSInvocation的形式将消息转发给多个对象,但是其开销比较大,需要创建新的NSInvocation对象,并且forwardInvocation的函数经常被使用者调用来做消息的转发选择机制,不适合多次重写
  3. forwardingTargetForSelector可以将消息转发给一个对象,开销较小,并且被重写的概率较低,适合重写

我们可以重写NSObject的该方法,可以做以下几步的处理:

  • 第一步:为类动态的创建一个桩类
  • 第二步:为类动态为桩类添加对应的Selector,用一个通用的返回0的函数来实现该SEL的IMP
  • 第三步:将消息直接转发到这个桩类对象上。

具体的代码实现如下:

- (id)yh_forwardingTargetForSelector:(SEL)aSelector {
    if(class_respondsToSelector([self class], @selector(forwardInvocation:))) {
        IMP impOfNSObject = class_getMethodImplementation([NSObject class], @selector(forwardInvocation:));
        IMP imp = class_getMethodImplementation([self class], @selector(forwardInvocation:));
        if (imp != impOfNSObject) {
            NSLog(@"class has implemented invocation");
            return nil;
        }
    }
    
    YHUnrecognizedSelectorSolveObject * solveObject = [YHUnrecognizedSelectorSolveObject new];
    solveObject.objc = self;
    return solveObject;
}

注意如果对象的类本身如果重写了forwardInvocation方法的话,就不应该对forwardingTargetForSelector进行重写了,否则会影响到该类型的对象原本的消息转发流程。

2、KVO类型crash防护

KVO crash 产生的原因大致有2种:
  • KVO的被观察者dealloc时仍然注册着KVO导致的crash
  • 添加KVO重复添加观察者或重复移除观察者(KVO注册观察者与移除观察者不匹配)导致的crash
未命名文件.jpg

一个被观察的对象上有若干个观察者,每个观察者又有若干条keypath,如果观察者和keypathx的数量一多,很容易不清楚被观察的对象整个KVO关系,导致被观察者在dealloc的时候,仍然残存着一些关系没有被注销,同时还会导致KVO注册者和移除观察者不匹配的情况发生,尤其是多线程的情况下,导致KVO重复添加观察者或者移除观察者的情况,这种类似的情况通常发生的比较隐蔽,很难从代码的层面上排查

KVO crash 防护方案

如何管理混乱的KVO关系呢:
可以让观察对象持有一个KVO的delegate,所有和KVO相关的操作均通过delegate来进行管理,delegate通过建立一张MAP表来维护KVO的整个关系,如下图:

未命名文件.jpg

这样做的好处有2个:

  1. 如果出现KVO重复添加观察或者移除观察者(KVO注册者不匹配的)情况,delegate,可以直接阻止这些非正常的操作。
  2. 被观察对象dealloc之前,可以通过delegate自动将与自己有关的KVO关系都注销掉,避免了KVO的被观察者dealloc时仍然注册着KVO导致的crash

3、NSNotification类型crash防护

当一个对象添加了notification之后,如果dealloc的时候,仍然持有notification,就会出现NSNotification类型的crash
NSNotification类型的crash多产生于程序员写代码时候犯疏忽,在NSNotificationCenter添加一个对象为observer之后,忘记了在对象dealloc的时候移除它。
所幸的是,苹果在iOS9之后专门针对于这种情况做了处理,所以在iOS9之后,即使开发者没有移除observer,Notification crash也不会再产生了。
不过针对于iOS9之前的用户,我们还是有必要做一下NSNotification Crash的防护的。
NSNotification Crash的防护原理很简单,利用method swizzling hook NSObject的dealloc函数,在对象真正dealloc之前先调用一下:[[NSNotificationCenter defaultCenter] removeObserver:self],即可。
注意:并不是所有的对象都需要做以上的操作,如果一个对象从来没有被NSNotificationCenter 添加为observer的话,在其dealloc之前调用removeObserver完全是多此一举。

4、NSTimer类型crash防护

NSTimer crash 产生原因:

在程序开发过程中,大家会经常使用定时任务,但使用NSTimer的 scheduledTimerWithTimeInterval:target:selector:userInfo:repeats: 接口做重复性的定时任务时存在一个问题:NSTimer会 强引用 target实例,所以需要在合适的时机invalidate 定时器,否则就会由于定时器timer强引用target的关系导致 target不能被释放,造成内存泄露,甚至在定时任务触发时导致crash。crash的展现形式和具体的target执行的selector有关。与此同时,如果NSTimer是无限重复的执行一个任务的话,也有可能导致target的selector一直被重复调用且处于无效状态,对app的CPU,内存等性能方面均是没有必要的浪费。所以,很有必要设计出一种方案,可以有效的防护NSTimer的滥用问题。

NSTimer crash 防护方案:

NSTimer所产生的问题的主要原因是因为其没有再一个合适的时机invalidate,同时还有NSTimer对target的强引用导致的内存泄漏问题。
那么解决NSTimer的问题的关键点在于以下两点:

  • NSTimer对其target是否可以不强引用
  • 是否找到一个合适的时机,在确定NSTimer已经失效的情况下,让NSTimer自动invalidate

关于第一个问题,target的强引用问题。可以用如下图的方案来解决:

未命名文件.jpg

在NSTimer和target之间加入一层stubTarget,stubTarget主要做为一个桥接层,负责NSTimer和target之间的通信。同时NSTimer强引用stubTarget,而stubTarget弱引用target,这样target和NSTimer之间的关系也就是弱引用了,意味着target可以自由的释放,从而解决了循环引用的问题。
上文提到了stubTarget负责NSTimer和target的通信,其具体的实现过程又细分为两大步:

  • swizzle NSTimer中scheduledTimerWithTimeInterval:target:selector:userInfo:repeats: 相关的方法,在新方法中动态创建stubTarget对象,stubTarget对象弱引用持有原有的target,selector,timer,targetClass等properties。然后将原target分发stubTarget上,selector回调函数为stubTarget的fireProxyTimer
  • 通过stubTarget的fireProxyTimer:来具体处理回调函数selector的处理和分发,当NSTimer的回调函数fireProxyTimer:被执行的时候,会自动判断原target是否已经被释放,如果释放了,意味着NSTimer已经无效,此时如果还继续调用原有target的selector很有可能会导致crash,而且是没有必要的。所以此时需要将NSTimer invalidate,然后统计上报错误数据。如此一来就做到了NSTimer在合适的时机自动invalidate

5、Container类型crash防护

Container crash 产生原因:

Container类型的crash 指的是容器类的crash,常见的有NSArray/NSMutableArray/NSDictionary/NSMutableDictionary/NSCache的crash。 一些常见的越界,插入nil,等错误操作均会导致此类crash发生。该类crash虽然比较容易排查,但是其在app crash概率总比还是挺高,所以有必要对其进行防护。

Container crash 防护方案:

Container crash 类型的防护方案也比较简单,针对于NSArray/NSMutableArray/NSDictionary/NSMutableDictionary/NSCache的一些常用的会导致崩溃的API进行method swizzling,然后在swizzle的新方法中加入一些条件限制和判断,从而让这些API变的安全。

6、野指针类型的crash

野指针类型crash产生的原因:

在App的所有Crash中,访问野指针导致的Crash占了很大一部分,野指针类型crash的表现为:Exception Type:SIGSEGV,Exception Codes: SEGV_ACCERR。
解决野指针导致的crash往往是一件棘手的事情,一来产生crash 的场景不好复现,二来crash之后console(控制台)的信息提供的帮助有限。XCode本身为了便于开放调试时发现野指针问题,提供了Zombie机制,能够在发生野指针时提示出现野指针的类,从而解决了开发阶段出现野指针的问题。然而针对于线上产生的野指针问题,依旧没有一个比较好的办法来定位问题。
所以,因为野指针出现概率高而且难定位问题,非常有必要针对于野指针专门做一层防护措施。

野指针crash 防护方案:

其实网上提出的方法都不完美,而且相当复杂。网上大多是在类init初始化的时候做一个标记,然后再dealloc再做一次标记,通过2次的标记来判断是否有内存,对于UIView UIImageview常用的类来讲多次分配释放内存消耗还是比较大的,并不是完美的解决方案。

可以使用腾讯的MLeaksFinder

MLeaksFinder 一开始从 UIViewController 入手。我们知道,当一个 UIViewController 被 pop 或 dismiss 后,该 UIViewController 包括它的 view,view 的 subviews 等等将很快被释放(除非你把它设计成单例,或者持有它的强引用,但一般很少这样做)。于是,我们只需在一个 ViewController 被 pop 或 dismiss 一小段时间后,看看该 UIViewController,它的 view,view 的 subviews 等等是否还存在。
具体的方法是,为基类 NSObject 添加一个方法 -willDealloc 方法,该方法的作用是,先用一个弱指针指向 self,并在一小段时间(2秒)后,通过这个弱指针调用 -assertNotDealloc,而 -assertNotDealloc 主要作用是直接中断言。这样,当我们认为某个对象应该要被释放了,在释放前调用这个方法,如果2秒后它被释放成功,weakSelf 就指向 nil,不会调用到 -assertNotDealloc 方法,也就不会中断言,如果它没被释放(泄露了),-assertNotDealloc 就会被调用中断言。这样,当一个 UIViewController 被 pop 或 dismiss 时(我们认为它应该要被释放了),我们遍历该 UIViewController 上的所有 view,依次调 -willDealloc,若2秒后没被释放,就会中断言。
总结起来一句话就是,当一个对象2秒之后还没释放,那么指向它的 weak 指针还是存在的,所以可以调用其 runtime 绑定的方法 willDealloc 从而提示内存泄漏。

7、非主线程刷UI类型crash防护

目前初步的处理方案是swizzle UIView类的以下三个方法:

- (void)setNeedsLayout;
- (void)setNeedsDisplay;
- (void)setNeedsDisplayInRect:(CGRect)rect;

在这三个方法调用的时候判断一下当前的线程,如果不是主线程的话,直接利用 dispatch_async(dispatch_get_main_queue(), ^{ //调用原本方法 });
来将对应的刷UI的操作转移到主线程上,同时统计错误信息。

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

推荐阅读更多精彩内容

  • 作为一个资深的技术团队,app的性能是我们技术团队首要的任务,其中最主要的一项就是app的崩溃率。目前虽然不能把系...
    kvoxxx阅读 1,062评论 2 1
  • 应用程序的崩溃总是最让人头疼的问题,也是非常严重的研发事故,那么应该如果降低程序的崩溃率呢?这里就用到了“APP运...
    光之盐汽水阅读 2,453评论 0 4
  • iOS Crash不崩溃 用户在使用App的过程中,经常遇到闪退的情况,体验不太好,SDK开发公司应更注重这个...
    粤西不出门阅读 845评论 0 1
  • 用户在使用App的过程中,经常遇到闪退的情况,体验不太好,本文尝试探索引发闪退的原因,以及在遇到crash的情况下...
    卫博生阅读 1,228评论 0 7
  • 原文地址 前言 正在运行的 APP 突然 Crash,是一件令人不爽的事,会流失用户,影响公司发展,所以 APP ...
    南栀倾寒阅读 6,125评论 23 81