问题简介
一个普通的拍照上传APP,功能稳定。产线反馈,20多分钟到半个小时左右,程序会崩溃退出。
这个问题隐藏很深,过程曲折,绕过好几次湾,最后不经意间有了发现。
过程中一直觉得不可思议,甚至开始怀疑一些常识。
最后,找到原因时,又感觉一切都顺理成章,合情合理。
探索过程
探索1:加入日志
-
加入日志,是Java后台,Linux定制开发等领域的常用手段;
-
OC有自己的NSLog,基本可用;但是直接用NSLog打日志,感觉不大好;所以一般都会来一个宏定义;调试版本输入日志,产线版本关闭日志:
#ifdef DEBUG
#define NSLog(...) NSLog(__VA_ARGS__)
#else
#define NSLog(...)
#endif
- 后来,又引入第三方库CocoaLumberjack进行日志分级管理;
到这里,日志的本地展示是没有问题了。
但是接入之后,发现对于问题的解决帮助不大:
日志在工厂手机上,手中的测试机一直没有遇到这个问题。如果想把保存的本地日志上报,需要后台提供接口。一来二去,就耽搁了。
探索2:接入友盟统计
-
友盟也算是老朋友了,从2012年入行iOS开发就听说过,当然,那个时候还主要用来做分享。现在,听说能收集崩溃信息了,所以考虑接入。
-
接入很简单,但是发现延时严重。免费版功能被阉割,数据要延时一天。生产数据没有发现,所以就人为制造几个崩溃进行尝试,比如给字典设置nil,数组越界什么的,才发现了这个延时问题。
-
符号表的上传倒是还可以,有个专门的页面做这个事,学习成本不高。
-
最近,友盟的崩溃统计,免费版已经看不到了,需要交钱成为会员才行。
没看到崩溃数据,还有延时问题,还要交钱,基本上放弃尝试了。
探索3:接入Bugly
-
听朋友介绍,腾讯的Bugly在搜集崩溃日志方面做得比友盟好,所以就尝试接入。
-
需要QQ登录。这也让我重新激活了已经放弃的QQ。绑定手机,安全验证,找回登录密码等等,好一顿折腾。有了QQ之后,登录还算方便,只要用QQ扫一扫就可以了。
-
接入之后,同样没有收集到产线的崩溃日志。这让我怀疑友盟和Bugly是不是浪得虚名。后来又想想,又不大会。可是,产线明明说有崩溃,可是崩溃日志一个都没收集到。我相信产线不会误报,连视频都有。友盟和Bugly同时失效的概率也很低。这真的会让人开始怀疑常识。
-
自己制造几个崩溃,Bugly确实比友盟好用。基本上等几分钟就能在网站上看到数据。版本号,崩溃原因什么的都写得很清晰。
-
Bugly的符号表上传麻烦多了,需要下载专门的工具,还需要专门的命令,比如下面这样的,不看文档,真不明白怎么用.
// bugly 符号表上传
java -jar buglyqq-upload-symbol.jar -appid 1c6e6552f2 -appkey cdc36a34-6895-4ee7-a405-8cffd644ad56 -bundleid com.wegobuy.PandaPhoto -version 1.7.4.010704 -platform IOS -inputSymbol PandaPhoto.app.dSYM
- 曾经也考虑过自己抓崩溃日志,然后通过Bugly的接口上报。不过想想还是算了。抓崩溃日志本来就是Bugly的核心功能,没有必要再做一次。
探索4:接入AvoidCrash
-
产线确实发生了崩溃,友盟和Bugly都没有抓到。看不到日志,找不到崩溃点,也无从下手。
-
既然AvoidCrash可以避免崩溃,那么就先引入再说。当时想法很简单:“我管你在哪里崩溃,我让你无法崩溃不就行了?”
-
AvoidCrash和Bugly还可以结合使用。一方面可以在发生崩溃的时候避免崩溃,还可以把崩溃信息提交到Bugly服务器,通过Bugly网页查看。
-
接入也很简单,只需要两步就可以:第1步侦听消息AvoidCrashNotification
第1步侦听消息AvoidCrashNotification
+ (void)setupAvoidCrash {
[AvoidCrash makeAllEffective];
// 防止unrecognized selector sent to instance的类有下面几个
NSArray *noneSelClassStrings = @[
@"NSNull",
@"NSNumber",
@"NSString",
@"NSDictionary",
@"NSArray"
];
[AvoidCrash setupNoneSelClassStringsArr:noneSelClassStrings];
//监听通知:AvoidCrashNotification, 获取AvoidCrash捕获的崩溃日志的详细信息
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(dealwithCrashMessage:) name:AvoidCrashNotification object:nil];
}
// 第2步上报崩溃消息到Bugly
+ (void)dealwithCrashMessage:(NSNotification *)note {
//异常拦截并且通过bugly上报
NSDictionary *info = note.userInfo;
NSString *errorReason = [NSString stringWithFormat:@"【ErrorReason】%@========【ErrorPlace】%@========【DefaultToDo】%@========【ErrorName】%@", info[@"errorReason"], info[@"errorPlace"], info[@"defaultToDo"], info[@"errorName"]];
NSArray *callStack = info[@"callStackSymbols"];
[Bugly reportExceptionWithCategory:3 name:@"AvoidCrash拦截的异常" reason:errorReason callStack:callStack extraInfo:@{} terminateApp:NO];
}
- 这个确实有作用,从Bugly网站看到了标签为@"AvoidCrash拦截的异常" 的异常列表,提供信息对于接Bug非常有帮助。
-
根据提示,相应的地方做了nil判断处理。只是改了之后,产线验证,半小时候还是出现了崩溃,还进行了录屏。这就让人很抓狂,明明解决了,也没有收到新的异常报告,怎么还有?
探索5:try catch
-
实在没办法了,只能死马当活马医:
在整个照片上传过程中都加入try catch结构。 -
OC是回调函数方式进行异步处理的,加try catch结构真的是差劲到极点。
-
当然是没作用的,只是心理安慰而已。try catch结构用在async await的模式中很好,在回调函数模式中,真的不要用。代码丑陋,还没作用。
探索6:内存泄露
-
反复看了几遍崩溃视频(时长31分钟,崩溃发生在最后20秒),发现崩溃发生时,拍照页面刚刚返回,还没有点“提交”按钮。也就是说,图片上传过程还没开始就发生了崩溃退出的现象。
-
拍照界面返回之后,回传拍照所得的图片。让后在上传页面设置,展示照片,内容很少啊,怎么会崩溃呢?是不是发生内存泄露?
-
在拍照的dealloc方法中加入log,看看是否正常销毁页面:
- (void)dealloc {
// 停止扫描
[self stop];
// 停止位置检测
if (self.isLevelDetection) {
[self stopMotionManager];
}
// 添加控制台打印;看是否正常退出
PDALogInfo(@"PDATakePhotoViewController dealloc");
}
尝试的结果,发现拍照页面退出了,上传页面也能正常设置照片,并且显示出来,但是PDATakePhotoViewController dealloc这句话在控制台一直没有看到。
到这里,产线所说的崩溃问题算是找到了原因:不是崩溃,而是内存泄露导致内存耗尽退出应用。
这就很好解释了为什么友盟和Bugly都看不到崩溃日志;接入了AvoidCrash也没能阻止;try catch结构也不起作用。以上几个让人怀疑常识的地方现在都顺利成章了。
解决过程
尝试1:引用循环
-
这里涉及到页面回传数据的问题,采用的是block方式,所以首先想到的就是引用循环。
-
调用点代码如下:
// 调用相机
[PDACamera takePhotosOn:self maxPhotoNumber:photoNumber isLevelDetection:YES success:^(NSArray<UIImage *> * _Nonnull images) {
/**
* 按顺序设置照片
*/
for (NSInteger i = 0; i < images.count; i++) {
self.imageView.image = images[i];
}
在这里加了weakSelf和strongSelf,不起作用,PDATakePhotoViewController dealloc这句话仍然没看到。
这里的block并不属于self,所以后来想想没有引用循环合情合理。
- 退出点代码:
// 退出并执行block
[self dismissViewControllerAnimated:YES completion:^{
if (self.success != nil) {
self.success([self.selectPhotos copy]);
}
}];
在这里加了weakSelf和strongSelf,不起作用,PDATakePhotoViewController dealloc这句话仍然没看到。
这里看上去像引用循环,其实没有。因为completion那个block并不属于self
尝试2:计时器
-
计时器不销毁也有可能导致页面无法退出。这里用了两个计时器,一个是延时执行,另外一个是按钮防抖。
-
计时器1: 延时执行
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
// 延时0.5s,自动对焦一次
[PDAHUD show:@"初始化相机... ..."];
[NSTimer scheduledTimerWithTimeInterval:0.5 repeats:NO block:^(NSTimer * _Nonnull timer) {
// 页面展示出来,就自动调整一次
[self focusAndExposureAtPoint:CGPointMake(0.5, 0.5)];
[PDAHUD dismiss];
[timer invalidate];
}];
}
- 计时器2: 按钮防抖
// 照相按钮
- (IBAction)takePhotoButtonTouched:(id)sender {
// 防抖,0.5秒
self.photoButton.enabled = NO;
[NSTimer scheduledTimerWithTimeInterval:0.5 repeats:NO block:^(NSTimer * _Nonnull timer) {
self.photoButton.enabled = YES;
[timer invalidate];
}];
}
- 简单粗暴,直接注释上面两块代码,隔离计时器。结果仍然没用,PDATakePhotoViewController dealloc这句话还是没看到。
尝试3:转移dealloc代码
- 原来的dealloc代码如下:
- (void)dealloc {
// 停止扫描
[self stop];
// 停止位置检测
if (self.isLevelDetection) {
[self stopMotionManager];
}
// 添加控制台打印;看是否正常退出
PDALogInfo(@"PDATakePhotoViewController dealloc");
}
PDATakePhotoViewController dealloc这句话还是没看到。存在内存泄露
- 修改后dealloc代码如下:
- (void)dealloc {
// 这里不能做耗时任务,否则会导致dealloc不执行,内存泄露
// 添加控制台打印;看是否正常退出
PDALogInfo(@"PDATakePhotoViewController dealloc");
}
能看到PDATakePhotoViewController dealloc这句话。内存泄露解除。
网友评论