- 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)
问题和解决
一:Unrecognized Selector类型crash防护
unrecognized selector类型的crash在app众多的crash类型中占着比较大的成分,通常是因为一个对象调用了一个不属于它方法的方法导致的。
二:KVO类型crash防护(NSNotification)
kVO crash 产生的原因:大致有2种
第一种:KVO的被观察者dealloc时仍然注册着KVO导致的crash
第二种:添加KVO重复添加观察者或重复移除观察者(KVO注册观察者与移除观察者不匹配)导致的crash
一个被观察的对象上有若干个观察者,每个观察者又有若干条keypath.
如果观察者和keypath的数量一多,很容易不清楚被观察的对象整个KVO关系,导致被观察者在dealloc的时候,
仍然残存着一些关系没有被注销,同时还会导致KVO注册者和移除观察者不匹配的情况发生
尤其是多线程的情况下,导致KVO重复添加观察者或者移除观察者的情况,这种类似的情况通常发生的比较隐蔽,很难从代码的层面上排查
KVO crash 防护方案
如何管理混乱的KVO关系呢:
可以让观察对象持有一个KVO的delegate,所有和KVO相关的操作均通过delegate来进行管理,delegate通过
建立一张MAP表来维护KVO的整个关系,如下图:
这样做的好处有2个:
1:如果出现KVO重复添加观察或者移除观察者(KVO注册者不匹配的)情况,delegate,可以直接阻止这些非正常的操作。
2:被观察对象dealloc之前,可以通过delegate自动将与自己有关的KVO关系都注销掉,避免了KVO的被观察者dealloc时仍然注册着KVO导致的crash
三:NSNotification类型crash防护(NSNotification)
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完全是多此一举
四:NSTimer类型crash防护(NSTimer)
4.1 NSTimer crash 产生原因
在程序开发过程中,大家会经常使用定时任务,但使用NSTimer的 scheduledTimerWithTimeInterval:target:selector:
userInfo:repeats: 接口做重复性的定时任务时存在一个问题:NSTimer会 强引用 target实例,所以需要在合适的时机invalidate 定时器,否则就会由于定时器timer强引用target的关系导致 target不能被释放,造成内存泄露,甚至在定时任务触发时导致crash。 crash的展现形式和具体的target执行的selector有关。
与此同时,如果NSTimer是无限重复的执行一个任务的话,也有可能导致target的selector一直被重复调用且处于无效状态,对app的CPU,内存等性能方面均是没有必要的浪费。所以,很有必要设计出一种方案,可以有效的防护NSTimer的滥用问题。
4.2 NSTimer crash 防护方案
上面的分析可见,NSTimer所产生的问题的主要原因是因为其没有再一个合适的时机invalidate,同时还有NSTimer对target的强引用导致的内存泄漏问题。
那么解决NSTimer的问题的关键点在于以下两点:
>1.NSTimer对其target是否可以不强引用
>2.是否找到一个合适的时机,在确定NSTimer已经失效的情况下,让NSTimer自动invalidate
关于第一个问题,target的强引用问题。 可以用如下图的方案来解决:
在NSTimer和target之间加入一层stubTarget,stubTarget主要做为一个桥接层,负责NSTimer和target之间的通信。
同时NSTimer强引用stubTarget,而stubTarget弱引用target,这样target和NSTimer之间的关系也就是弱引用了,意味着target可以自由的释放,从而解决了循环引用的问题。
上文提到了stubTarget负责NSTimer和target的通信,其具体的实现过程又细分为两大步:
step 1. swizzle NSTimer中scheduledTimerWithTimeInterval:target:selector:userInfo:repeats: 相关的方法,在新方法中动态创建stubTarget对象,stubTarget对象弱引用持有原有的target,selector,timer,targetClass等properties。然后将原target分发stubTarget上,selector回调函数为stubTarget的fireProxyTimer
step 2. 通过stubTarget的fireProxyTimer:来具体处理回调函数selector的处理和分发
当NSTimer的回调函数fireProxyTimer:被执行的时候,会自动判断原target是否已经被释放,如果释放了,意味着NSTimer已经无效,此时如果还继续调用原有target的selector很有可能会导致crash,而且是没有必要的。所以此时需要将NSTimer invalidate,然后统计上报错误数据。如此一来就做到了NSTimer在合适的时机自动invalidate
五:Container类型crash防护(Container)
5.1 Container crash 产生原因
Container 类型的crash 指的是容器类的crash,常见的有NSArray/NSMutableArray/NSDictionary/NSMutableDictionary/NSCache的crash。 一些常见的越界,插入nil,等错误操作均会导致此类crash发生。由于产生的原因比较简单,就不展开来描述了。
该类crash虽然比较容易排查,但是其在app crash概率总比还是挺高,所以有必要对其进行防护
5.2 Container crash 防护方案
Container crash 类型的防护方案也比较简单,针对于NSArray/NSMutableArray/NSDictionary/NSMutableDictionary/
NSCache的一些常用的会导致崩溃的API进行method swizzling,然后在swizzle的新方法中加入一些条件限制和判断,
从而让这些API变的安全,这里就不展开来具体描述了。
六:野指针类型的crash
6.1:野指针产生的原因
在App的所有Crash中,访问野指针导致的Crash占了很大一部分,野指针类型crash的表现为:Exception Type:SIGSEGV,Exception Codes: SEGV_ACCERR
解决野指针导致的crash往往是一件棘手的事情,一来产生crash 的场景不好复现,二来crash之后console的信息提供的帮助有限。
XCode本身为了便于开放调试时发现野指针问题,提供了Zombie机制,能够在发生野指针时提示出现野指针的类,
从而解决了开发阶段出现野指针的问题。然而针对于线上产生的野指针问题,依旧没有一个比较好的办法来定位问题。
所以,因为野指针出现概率高而且难定位问题,非常有必要针对于野指针专门做一层防护措施
6.2 野指针crash 防护方案
其实网上提出的方法都不完美,而且相当复杂。网上大多是在类init初始化的时候做一个标记,然后再dealloc再做一次标记,通过2次的标记来判断是否有内存,对于UIView UIImageview常用的类来讲多次分配释放内存消耗还是比较大的,并不是完美的解决方案
这里教大家一个小技巧:
大家知道怎么判断一个实例的内存是否已经释放了吗?这个方法是我发现的,亲测,非常有效,用于判断当前指针的内存是否还在
if(!malloc_zone_from_ptr((__bridge const void *)(strongself)))return;
但是它也不能解决全部的问题
因为我们不知道什么时候去调用类函数什么时候调用属性
七:非主线程刷UI类型crash防护(UI not on Main Thread)
目前初步的处理方案是swizzle UIView类的以下三个方法:
-(void)setNeedsLayout;
-(void)setNeedsDisplay;
-(void)setNeedsDisplayInRect:(CGRect)rect;
在这三个方法调用的时候判断一下当前的线程,如果不是主线程的话,直接利用 dispatch_async(dispatch_get_main_queue(), ^{ //调用原本方法 });
来将对应的刷UI的操作转移到主线程上,同时统计错误信息。
但是真正实施了之后,发现这三个方法并不能完全覆盖UIView相关的所有刷UI到操作,但是如果要将全部到UIView的刷UI的方法统计起来并且swizzle,感觉略笨拙而且不高效。 但是这种crash占比并不高,我们重要的宗旨是降低再降低crash率,不是彻底的完全消灭,而且我们目前也没有办法完全消灭,只有我们掌握了底层的原理,才能灵活应变处理问题!
今天的文章iOS中常见Crash总结以及解决方案分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/15776.html