美文网首页
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这句话。内存泄露解除。

    相关文章

      网友评论

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

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