内容字号:默认大号超大号

段落设置:段首缩进取消段首缩进

字体设置:切换到微软雅黑切换到宋体

再谈iOS App Crash防护

2018-06-08 18:56 出处:清屏网 人气: 评论(0

在移动开发中,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 ,这类异常如果没有被处理掉的话,会被转发到 SIGBUSSIGSEGV 等类型的 BSD 信号;
  • 由 SDK 开发者或上层应用开发者主动抛出的异常信号,比如各种常见的 NSException ,这类异常苹果为了统一处理,最终会被转发为 SIGABRT 类的 BSD 信号;
  • 其他进程杀死你的应用;

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

可选的 Crash 防护方案

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

通过 NSUncaughtExceptionHandler 来捕获并处理异常

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

NSSetUncaughtExceptionHandler(&HandleException);

通过 BSD 的 signal 来捕获并处理异常

由于苹果将所有异常最终都转换成了 BSD 信号的发出,那么我们就可以去捕获这个信号来处理这些异常,从而达到 Crash 防护的目的。系统也有提供相关 API 实现:

void    (*signal(int, void (*)(int)))(int);

前一个参数为异常类型,可以是 SIGSEGV 等这类,后一个参数为回调的函数,代码写起来就可以是:

signal(SIGABRT, SignalHandler);
signal(SIGILL, SignalHandler);
signal(SIGSEGV, SignalHandler);
signal(SIGFPE, SignalHandler);
signal(SIGBUS, SignalHandler);
signal(SIGPIPE, SignalHandler);

注意:由于 Xcode 默认会开启 debug executable ,它会在我们捕获这些异常信号之前拦截掉,因此做这个测试需要手动将 debug executable 功能 关闭 ,或者不在 Xcode 连接调试下进行测试。

至此,似乎一切看起来都很顺利,然而实践过程中你会发现程序并没有在你处理完这些异常后就能继续进行。这与 iOS 的 Runloop 机制有关,在触发异常后, Main Runloop 将不会继续运行,这也就意味着 App 跑不起来了。当然,你可能会很自然地联想到,我自己再把 Main Runloop 继续挂起来跑不就行了吗?如以下类似代码:

//这里取到的是 Main Runloop
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop);

while (YES)
{
    for (NSString *mode in (NSArray *)allModes)
    {
        CFRunLoopRunInMode((CFStringRef)mode, 0.001, false);
    }
}

CFRelease(allModes);

这样一试,确实程序在捕获异常之后又能够继续运行了。但『通过 NSUncaughtExceptionHandler 来捕获并处理异常』和『通过 BSD 的 signal 来捕获并处理异常』这两种方式去做 Crash 防护并不是一种靠谱的方式,原因有以下几点:

  • iOS/OSX 在被抛出异常后,被认为是不可恢复的,如果我们强行恢复 Runloop,整个 App 的不确定性将会更大,crash 的部分可能会再次发生;
  • 内核抛出的异常一般都是较严重的底层硬件问题,如果这类问题不及时停止程序运行,可能会进一步影响整个系统的运行,乃至损坏硬件;
  • 以上两种做法,通常是用于 Crash 日志收集上,如果我们防护层也通过这个方案去做的话,冲突的可能性会很大;

这里附带下『App Architecture book』作者 Matt Gallagher 早年对于这部分研究后的一个 demo ,由于是 MRC 时代的代码了,修改了部分配置使得能够正常编译且测试。

通过 try-catch 的组合拳来捕获异常

和其他编程语言一样,Objective-C 中也有万能的 try-catch 组合来捕获异常,这样处理不就可以了?这种方案确实是可行的,我也确实有见过一些人使用 try-catch 来做一些常见的 Crash 防护。但 Objective-C 的 try-catch 实际上有先天缺陷的,首先是效率并不高,甚至某些情况下会导致内存泄漏,不可控。

  • 效率不高是由于 try-catch 是基于 block 的处理方案,会多出额外的开销(不过苹果已经重写了 64 bit 机器上的 try-catch,而且声明是 zero-cost );
  • 可能会内存泄漏是由于 Xcode 默认并不会对 try-catch 中的代码进行 ARC 管理。try 在捕捉到 Exception 之后,会立即转到 catch 中执行,这样就导致了如果 release 代码是写在 try 中 throw 异常的代码之后的话,就会不被执行而导致内存泄漏。如果为了防止这个泄漏而去配置 -fobjc-arc-exceptions 选项,更会因为生成低效代码而得不偿失,这也是苹果并不推荐的方式。

但这不能完全否定 try-catch 组合在我们日常编程中的作用,在一些容易出现异常的操作上,比如文件读写或者需要配合使用 throw 的情况等。这里指的不适合,只是针对在大范围防护并不适合。

Baymax 的方案

在综合分析了以上几个防护方案后,我们再来看看 Baymax 中采用的方案。如果说上面三种方案都是在已经抛出了异常之后再去捕获处理,也就是 “喝后悔药” 的机制,那么 Baymax 的方案便是 不让这些异常产生 。不让错误异常产生可以通过多种做法,往项目管理上说提高代码质量,增加 Code Review 等,从编码角度来说,我们可以通过各种 保护性代码 进行。Baymax 中的大部分防护方案都可以理解为一种为你自动增加保护性代码的措施。比如,各种 Collection 类型,String 类型等。

实践 Baymax 方案中可能遇到的问题

高频调用方法的性能问题

Baymax 是基于 AOP 思想而设计的,方案中会充斥着各种 Hook 系统方法,这对于高频调用的方法,性能上的损耗是不可忽略的。为了将损耗尽量降低,我们可以通过只防护特定类来进行,比如只针对我们的自定义类和部分在防护名单内的类,而对于系统的类,我们不进行防护,这样就能在一定限度上降低性能损耗。对于判断自定义类可以通过以下方法进行:

如果只是判断 main bundle 的话可以通过以下代码进行:

+ (BOOL)isMainBundleClass:(Class)cls {
    return cls && [[NSBundle bundleForClass:cls] isEqual:[NSBundle mainBundle]] ;
}

但在组件化开发中,我们的代码会通过各种私有 pod 的形式导入,这样只判断 main bundle 的方式就不够用了,我们可以通过以下代码进行:

+ (BOOL)isCustomClass:(Class)cls {
    ///var/containers/Bundle/Application/CB0D354B-DD08-4845-A084-A22FF01097FE/Example.app
    NSString *mainBundlePath = [NSBundle mainBundle].bundlePath;
    ///var/containers/Bundle/Application/CB0D354B-DD08-4845-A084-A22FF01097FE/Example.app/Frameworks/Baymax.framework
    NSString *clsBundlePath = [NSBundle bundleForClass:cls].bundlePath;

    return cls && mainBundlePath && clsBundlePath && [clsBundlePath hasPrefix:mainBundlePath];
}

另外,由于判断是否防护的条件会相对比较多,这里可以引入 名单缓存 来做进一步的效率优化,将本次判断结果存储到 NSCache 中,下回优先从 Cache 里读取防护状态,性能提升将会十分显著。大致代码如下:

//先从缓存中读取状态
NSNumber *status = [baymax needBaymaxStatusInProtectionCache:clsStr];
//如果有在缓存中 则直接返回缓存中的状态 若不在缓存中 则继续走判断逻辑
if (status != nil) return [status boolValue];

UnrecognizedSelector 防护的坑

苹果在 KVO 的实现中,为每种类型都封装了一个特定的 set 方法,原因未知(或许又是 Historical Reasons 吧),这里涵盖了 CoreFoundation 里的所有基础类型。

_NSSetBoolValueAndNotify、_NSSetCharValueAndNotify、_NSSetDoubleValueAndNotify、_NSSetFloatValueAndNotify、_NSSetIntValueAndNotify、_NSSetLongLongValueAndNotify、_NSSetLongValueAndNotify、_NSSetObjectValueAndNotify、_NSSetPointValueAndNotify、_NSSetRangeValueAndNotify、_NSSetRectValueAndNotify、_NSSetShortValueAndNotify、_NSSetSizeValueAndNotify、_NSSetUnsignedCharValueAndNotify、_NSSetUnsignedIntValueAndNotify、_NSSetUnsignedLongLongValueAndNotify、_NSSetUnsignedLongValueAndNotify、_NSSetUnsignedShortValueAndNotify

分享给小伙伴们:
本文标签: CrashiOS

相关文章

发表评论愿您的每句评论,都能给大家的生活添色彩,带来共鸣,带来思索,带来快乐。

CopyRight © 2015-2016 QingPingShan.com , All Rights Reserved.

清屏网 版权所有 豫ICP备15026204号