当然崩溃率和日活是有关系的,我只能说我的APP肯定不是只有几万日活的APP。程序的稳定性不用我多说,其重要性是不言而喻的。如果APP动不动就崩溃,那就不用说什么交互什么用户体验了,用户的第一反应就是直接把APP删掉或者找替代你的APP。
如何降低崩溃率呢,先分一下一下崩溃的原因:
一、内存管理问题;
二、容错处理不完善;
三、webview与其他崩溃。
首先内存问题,我们不得不回顾以下历史,在很久很久以前的蛮荒时代,这个时代里,手机内存最小的只有128Mb,一个APP可用的内存更少,还要手动管理内存,程序猿们苦不堪言。
这个时代程序猿的内功心法是:谁创建,谁是释放;需要时申请,不需要时释放。但是实际写代码时,我们小心翼翼,也很难避免不出现问题。典型问题如
应该释放的内存忘了释放导致内存驻留;不该释放的你释放了,直接crash;内存已经释放了,指针不去置空,出现野指针。
庆幸的是13年以后,随着设备可用内存的增加,苹果强制使用自动内存管理(ARC)了。这个时候我们很不情愿的使用了ARC,虽然大家还在抨击苹果这个ARC真垃圾,清理内存不及时,不能有效控制内存的峰值,不如我们自己管理内存。但是我们要说这个ARC的确减轻了我们的负担,让我们编程更加的高效。在这个是时代里,原则就是只要有强指针指向这块内存,这块内存就会驻留,不会被释放;一旦没有强指针指向这块内存,这块内存就在系统方便的时候被回收了。这样程序猿就不用关心引用计数,不用release对象,野指针消失不见,崩溃明显减少。
ARC解决了大部分问题,但是我们要记住两点,一是使用cf对象的时候要记得自己创建的对象,自己记得release;而是避免循环引用。我遇到的问题是团队对于这种循环引用认识不足,因为即使有这种循环引用,APP照常运行,感觉不到什么问题,问题是感觉不到问题才是问题。我会问从内存管理方面APP为何会崩溃,回答是内存过大。其实问题在于内存峰值过高,系统出于保护自己的目的,shut了我们的APP。而导致内存峰值过高的罪魁祸首很大一部分来自于我们的内存泄漏。不断的内存泄漏,使得我们的APP占用内存越来越大,同时系统有不能及时清除,到达一定程度,APP运行开始缓慢甚至崩溃就不可避免了。delegate循环引用问题不大,基本上是block循环引用造成的问题。其实典型问题
__block TestModel*tModel = self.testModel;
self.testModel.bClick = ^{
[tModel.array addObject:@"1"];
[self pushNext];
};```
这段实例代码大家很熟悉,如果只能发现一处循环引用,就需要注意了。一方面self持有testModel对象,testModel持有bClick的block,block又调用self,持有了self导致循环引用。另一方面testModel不需要用block来修饰,同时testModel对象的block持有了testModel自身,造成循环引用。更多block内存问题,请自行谷哥。
再者就是容错处理问题,这个问题从两方面来考虑:
一方面,我们需要增强我们代码的健壮性,该容错的地方进行必要的容错处理,举个例子,我们常见的崩溃引发问题,如数组越界,setObject:forKey:空值,initWithString:空值,数据类型不匹配。如果不进行有效的容错判断,裸奔的效果就像内存泄漏一样,测试没问题,到了线上崩溃就出现了,特别是APP的体积越来越大,后台逻辑越来越复杂,用户越来越多的情况下。比如字符串,建议进行如下判断:
if (string && [string isKindOfClass:[NSString class]] && string.length > 0) {
}```
当然我使用了类别来拓展常用类型的判断使用时比较方便。类似这样:
NSStirng+Extension
+ (BOOL)isValid:(NSString *)string{
if (string && [string isKindOfClass:[NSString class]] && string.length > 0) {
return YES;
}else{
return NO;
}
}
使用时
if ([NSString isValid:@"good boy"]){
}
另一方面,我们如果处处都加这种判断,固然是好,但是总有漏网之鱼。另外我们不知道什么时候服务器就把字典传成了数组,不做处理的话就坑了,服务器出问题了我们客户端跟着崩,还说我们的代码不健壮。这个之后就需要利用运行时动态的替换方法来规避这种问题。例如setObject:forKey:问题,我们load的时候,进行方法替换:
+ (void)load{
Class dictCls = NSClassFromString(@"__NSDictionaryM");
Method originalMethod = class_getInstanceMethod(dictCls, @selector(setObject:forKey:));
Method swizzledMethod = class_getInstanceMethod(dictCls, @selector(lcx_setObject:forKey:));
if (!originalMethod || !swizzledMethod) {
return;}
method_exchangeImplementations(originalMethod, swizzledMethod);
}
- (void)lcx_setObject:(id)anObject forKey:(id)aKey{
if (anObject == nil) {
NSLog(@"crash---NSMutableDictionary set nil object");
return;
}
if (aKey == nil) {
NSLog(@"crash---NSMutableDictionary set nil key");
return;
}
[self lcx_setObject:anObject forKey:aKey];
}```
进一步引申,这个时候在崩溃的地方,我们是可以获取到堆栈信息的,我们可以把这些存起来,自建崩溃监控系统,来发现APP隐藏的crash。另外要注意的是这样hook目前只能拦截特定方法还不能拦截类型不匹配造成的unrecognized selector send to instance问题。
最后是webview与其他崩溃。webview占很大内存特别是UIWebView,所以单独和大家说一下,一定不要在webview的vc里面出现循环引用,这样可能会导致大量内存无法释放。另外,webview记得在dealloc中将delegate置为nil,同时删除缓存数据,减少其所占的内存。webview内的h5页面如果本身不注意内存管理或者一些bug也会造成崩溃,只能让前段多注意了。有时我们在观测崩溃的时候,发现一些webcore的崩溃,崩溃率出现峰值后来又趋于正常,很可能就是h5页面上线了一些bug页面后来修复了。大家可以设置接收APP崩溃的邮件,及时反馈,及时解决。(崩溃监控建议大家使用bugly,自动上传dysm,堆栈信息都解析出来了,不用自己手动解析堆栈信息)。
我们还遇到一些常见的崩溃,主要是大家的代码习惯问题了,如观察者或者通知中心忘移除、观察者移除崩溃、多次push同一个控制器、NSTimer等。这些问题大家自己多注意就OK了。最后一定要注意多线程读取数据的问题以及避免非主线程操作UI。
面对这些问题,我们好好做了,崩溃率自然会下降,但是依赖自查还是不能完全避免问题。这就要每次提测前用工具走一遍,查看是否还遗留有问题。我的习惯:
1.Analyze进行静态检查
2.Instruments的leaks进行动态分析
3.Instruments的Allocations分析一下是否有大内存占用
4.MLeakFinder查一下循环引用
最后希望大家还是要注意培养自己良好的代码习惯,清晰的数据结构,提高代码质量。
DEMO请移步:https://github.com/dudongdaoqi/LCXCrashExtension
References
[MLeakFinder](https://github.com/Zepo/MLeaksFinder)
[h5Crash研究](http://web.jobbole.com/86309/)