分享

再谈 iOS App Crash 防护

 路人甲Java 2021-12-22

在移动开发中,App 的闪退率是工程师十分关注且又头疼的事情。去年,网易杭州研究院曾经针对 crash 的防护有提出『大白健康系统--iOS APP 运行时 Crash 自动修复系统』方案,使得 crash 防护这个想法真正被落实,但至今该方案的具体实现并没有被开源。经过一年的时间,圈子里也有一些开发朋友,基于这套方案设计并开源了自己的 “Baymax”,比如『老司机 iOS 周报第七期』中曾提到的 BayMaxProtector。本文将会针对网易 Baymax 这套方案,结合团队内的实践结果,总结其在生产环境中可能遇到的问题及其解决方案,并提出一些自己对这套方案的思考。友情提示,阅读本文前需对网易『大白健康系统--iOS APP 运行时 Crash 自动修复系统』一文有所了解,该文中已有的实现方案,本文不会再花更多笔墨进行赘述。

Crash 防护可选的方案

Crash 是什么?

在探讨 Crash 防护的方案之前,我们有必要对计算机领域 Crash 这个概念进行重新认识。对于 Crash 的概念,维基百科中是这么定义的:

In computing, a crash (or system crash) occurs when a computer program, such as a software application or an operating system, stops functioning properly and exits.

An application typically crashes when it performs an operation that is not allowed by the operating system. The operating system then triggers an exception or signal in the application. Unix applications traditionally responded to the signal by dumping core. Most Windows and Unix GUI applications respond by displaying a dialogue box (such as the one shown to the right) with the option to attach a debugger if one is installed. Some applications attempt to recover from the error and continue running instead of exiting.

对于我们 iOS 应用层的 App,可简单总结为应用执行了某些不被允许的操作触发了系统抛出异常信号但又没有处理这些异常信号从而被杀掉的现象,比如常见的闪退(crash to desktop)。在我们开发领域从抛出异常的对象上来看,一共可以分为三类内核导致的异常、应用自身的异常或其他进程导致的异常:

  • 由操作系统内核捕获硬件产生的异常信号,比如 EXC_BAD_ACCESS,这类异常如果没有被处理掉的话,会被转发到 SIGBUS 或 SIGSEGV 等类型的 BSD 信号;
  • 由 SDK 开发者或上层应用开发者主动抛出的异常信号,比如各种常见的 NSException,这类异常苹果为了统一处理,最终会被转发为 SIGABRT 类的 BSD 信号;
  • 其他进程杀死你的应用;

这里我们主要谈最常见的前两种异常。

可选的 Crash 防护方案

上面已经提到了 Crash 实际上我们触发了异常,但又没有去处理这些异常而导致的结果。那么很自然的第一个防护方案便可以想到是去处理这些异常。

通过 NSUncaughtExceptionHandler 来捕获并处理异常

苹果的确提供有异常捕获的 API 以供开发者使用——NSSetUncaughtExceptionHandler,开发者只需要传入处理函数的指针,便可以处理掉应用中抛出的 NSException 类的异常。代码写起来就是:

- (void)testCrashProtection {
    //given when
    Baymax *baymax = [Baymax sharedInstance];
    [baymax configBaymaxType:BaymaxAll];
    [baymax start];

    //then
    for (int i = 0 ; i < kBaymaxType; i++) {
        NSUInteger type = 1 << i;
        Tester *tester = [Tester tester:type];

        NSUInteger caseCount = [[tester testCaseSelectors] count];

        for (int j = 0; j < caseCount; j++) {
            XCTAssertNoThrow([tester executeTestCase:j]);
        }
    }
}

防护的代价是什么

任何事物我们都从正反两方面考虑,既然 Baymax 提供了防护功能,那其必然也存在着弊端。

首先,第一点就是上面提到的性能问题,在方案调研阶段,笔者曾经使用 XCTest 对 Collection 类型的防护做了部分的性能测试,结果大致如下:

不做 Hook
Test Case '-[PerformanceTests testPerformance_Collection]' measured [Time, seconds] average: 0.000, relative standard deviation: 151.327%, values: [0.000011, 0.000002, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001]

做了 Hook 但是不触发防护逻辑
Test Case '-[PerformanceTests testPerformance_Collection]' measured [Time, seconds] average: 0.000, relative standard deviation: 83.636%, values: [0.000021, 0.000005, 0.000005, 0.000009, 0.000003, 0.000003, 0.000003, 0.000003, 0.000009, 0.000003]

做了 Hook 且触发了防护逻辑
Test Case '-[PerformanceTests testPerformance_Collection]' measured [Time, seconds] average: 0.000, relative standard deviation: 47.857%, values: [0.000026, 0.000010, 0.000009, 0.000009, 0.000008, 0.000009, 0.000009, 0.000008, 0.000009, 0.000009]

从上面数据可以很直观地看到,在不做任何优化的前提下性能下降十分明显,效率损失甚至高达 3 倍以上,所以如果要做防护,必须充分考虑到性能优化这些点。

其次,需要合理权衡开启的防护类型,目前我们仅默认开启线上反馈的常见类型,而不是开启所有类型,其他类型可以配置为动态开启,根据用户设备的闪退日志开启防护。其中,Baymax 中提到的野指针防护,在实践中发现用处很有限,因为只是做了延迟释放,而不是真正意义上对野指针这种 crash 进行防护,且由于对系统的释放时机进行了处理,与 Xcode 原来的 Zombie 机制有一定冲突,也会产生一些很奇葩的问题,不确定性很高。

再次,各种Hook带来的未知性,Crash 本身是非正常情况下才产生的,如果一味地规避这种异常,可能会产生更多的异常情况,特别是业务逻辑上会出现不可控制的流程。

最后,这套防护方案的作用究竟有多大呢?根据笔者个人经验来说,对于越成熟的团队,防护方案带来的效果会越小。因为成熟团队的代码质量相对更高,一些低级错误出现的概率极小。但对于小团队,或者历史比较久的项目而言,这套方案带来的帮助会比较大,毕竟坑总是防不胜防的。

推荐

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多