美文网首页
dealloc中不要执行耗时任务 2023-02-14 周二

dealloc中不要执行耗时任务 2023-02-14 周二

作者: 勇往直前888 | 来源:发表于2023-02-14 12:34 被阅读0次

问题简介

一个普通的拍照上传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非常有帮助。
AvoidCrash拦截的异常
  • 根据提示,相应的地方做了nil判断处理。只是改了之后,产线验证,半小时候还是出现了崩溃,还进行了录屏。这就让人很抓狂,明明解决了,也没有收到新的异常报告,怎么还有?

  • AvoidCrash的集成注意事项、疑惑的解答

探索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这句话。内存泄露解除。

相关文章

  • iOS话题:GCD-2020-04-28

    基本使用 GCD的基本使用场景就是避免耗时任务导致界面卡顿。基本思路: 将耗时任务放入全局并行队列中执行。 执行完...

  • GCD dispatch_group

    1、使用场景 1、异步执行多个耗时任务。2、当多个耗时任务都执行完回到主线程执行任务。 2、dispatch_gr...

  • HandlerThread原理及优缺点分析

    1、HandlerThread原理 当系统有多个耗时任务需要执行时,每个任务都会开启个新线程去执行耗时任务,这样会...

  • 2018-07-06

    service和AIDL service执行与UI进程中,所以不要在service中执行耗时操作。 service...

  • 关于正确使用Android AsyncTask学习整理

    AsyncTask异步任务,用于执行耗时任务并在UI线程中更新结果。 我们都知道,Android UI线程中不能执...

  • Android AsyncTask的使用学习整理

    AsyncTask异步任务,用于执行耗时任务并在UI线程中更新结果。 我们都知道,Android UI线程中不能执...

  • HandlerThread总结

    使用场景 程序需要执行一系列的耗时任务,这时候就需要启动额外的线程去执行耗时任务。如果每次遇到耗时任务都直接创建线...

  • GCD之dispatch_group详解

    dispatch_group 通常我们执行耗时操作会放到子线程中并发执行,这个过程中我们可能想知道各个任务全部执行...

  • dispatch_group_async队列组实现多线程异步操作

    分别异步执行2个耗时任务,然后当2个耗时任务都执行完毕后再回到主线程执行任务。这时候可以用到 GCD 的队列组。调...

  • iOS GCD (二 ) dispatch_group 队列组

    有时候我们会有这样的需求:分别异步执行2个耗时任务,然后当2个耗时任务都执行完毕后再回到主线程执行任务。这时候我们...

网友评论

      本文标题:dealloc中不要执行耗时任务 2023-02-14 周二

      本文链接:https://www.haomeiwen.com/subject/ziyskdtx.html