iOS拓展---常见crash以及解决方案
APP运行时Crash自动修复+捕获系统 的设计初衷,就是为了降低app的crash率。利用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)……
问题和解决
一: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
具体实现:见demo
三:NSNotification类型crash防护(NSNotification)
3.1 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完全是多此一举
具体实现:见demo
四: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
补充:众所周知,NSObject类是Objective-C中大部分类的基类。但不是很多人知道除了NSObject之外的另一个基类——NSProxy
NSProxy是一个虚类,你可以通过继承它,并重写这两个方法以实现消息转发到另一个实例
栗子:
/** 桥接层 NSTimer强引用WOCPWeakProxy, WOCPWeakProxy弱引用target 这样target和NSTimer之间的关系也就是弱引用了,意味着target可以自由的释放,从而解决了循环引用的问题 */ @interface WOCPWeakProxy: NSProxy @property (nonatomic, weak, readonly) id target; - (instancetype)initWithTarget:(id)target; + (instancetype)proxyWithTarget:(id)target; @end @implementation WOCPWeakProxy - (instancetype)initWithTarget:(id)target { _target = target; return self; } + (instancetype)proxyWithTarget:(id)target { return [[WOCPWeakProxy alloc] initWithTarget:target]; } //当不能识别方法时候,就会调用这个方法,在这个方法中,我们可以将不能识别的传递给其它对象处理 //由于这里对所有的不能处理的都传递给_target了,所以methodSignatureForSelector和forwardInvocation不可能被执行的,所以不用再重载了吧 //其实还是需要重载methodSignatureForSelector和forwardInvocation的,为什么呢?因为_target是弱引用的,所以当_target可能释放了,当它被释放了的情况下,那么
forwardingTargetForSelector就是返回nil了.然后methodSignatureForSelector和forwardInvocation没实现的话,就直接crash了!!! //这也是为什么这两个方法中随便写的!!! // 转发目标选择器 - (id)forwardingTargetForSelector:(SEL)selector { return _target; } // 函数执行器 - (void)forwardInvocation:(NSInvocation *)invocation { void *null = NULL; [invocation setReturnValue:&null]; } // 方法签名的选择器 - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel { return [NSObject instanceMethodSignatureForSelector:@selector(init)]; }
具体实现:见DEMO
五: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变的安全,这里就不展开来具体描述了。
具体实现见DEMO
六:野指针类型的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率,不是彻底的完全消灭,而且我们目前也没有办法完全消灭,只有我们掌握了底层的原理,才能灵活应变处理问题!