引言

“如果某个实体表现出以下任何一种特性,它就具备自主性:自我修复、自我保护、自我维护、对目标的自我控制、自我改进。” —— 凯文·凯利

iOS App 有时可能遇到启动必 crash 的绝境:每次打开 App 都闪退,无法正常使用App。

为了尝试解决这个问题,微信读书开发了 iOS 连续闪退保护工具:GYBootingProtection,检测连续闪退,在连续闪退出现时,尝试自修复 App:


本文探讨了连续闪退问题的产生原因、检测、修复机制,以及如何在你的项目中引入、测试和使用 GYBootingProtection

连续闪退检测

首先要检测用户 App 出现了连续闪退的情况,有两种检测方法,捕获异常和计时器。

1. 捕获异常

检测连续闪退,可以通过捕获异常来实现,异常有以下种类:

  • Mach 异常:EXC_CRASH

  • UNIX 信号:SIGABRT

  • NSException 异常:应用层,通过 NSUncaughtExceptionHandler 捕获

在念茜的漫谈 iOS Crash 收集框架一文中详细介绍了 Mach 异常和 Unix 信号捕获 crash 的机制。简单来说,异常一般产生自 iOS 的微内核 Mach,然后在 BSD 层转换成 UNIX SIGABRT 信号,以标准 POSIX 信号的形式提供给用户。NSException 是使用者在处理 App 逻辑时,用编程的方法抛出。

如何捕获异常

通过以下方法捕获异常:

  • 利用 Mach API 捕获 Mach 异常

  • 通过 POSIX API 注册 signal(SIGSEGV,signalHandler) 来捕获 UNIX 异常信号

  • 注册 NSUncaughtExceptionHandler 来捕获应用级异常

Crash 上报工具如 PLCrashReporter 通过注册 Mach 异常 + UNIX信号 的 handler 达到检测的目的,对用户提供了处理异常的接口。

如何检测

可以利用 PLCrashReporter 这类工具来检测连续闪退:

  1. 首先维护一个计数变量,表示连续闪退次数

  2. 在 PLCrashReporter 的 crash handler 中加入逻辑:如果启动 5s 内 crash 使计数器加一

  3. 每次启动时,如果连续闪退计数 > n,则检测到了连续闪退

  4. 启动后,执行一个定时任务,在 5s 后重置计数(如果 App 连续闪退则不会重置)

流程图

优缺点

通过 Mach 异常、Unix 信号、NSException 异常来检测闪退,能获得更多的 crash 上下文,但由于 crash 收集框架多使用这些方法,可能会有这样的风险:与第三方 crash 收集框架冲突导致漏检测。另外,可能会与 App 已有的异常处理代码产生耦合。

2. 计时器方法

除了通过捕获异常的方式检测连续闪退,还可以通过计数器方法来检测:

  1. 维护一个计数变量,用于表示连续闪退的次数

  2. 在启动 application:didFinishLaunchingWithOptions: 后使计数加一

  3. 接着使用 dispatch_after 方法在 5s 后清零计数,如果 App 活不过 5 秒计数就不会被清零

  4. 如果发现计数变量 > n,表明 App 连续 n 次连续闪退,启动保护流程,重置计数。

  5. 当保护流程完成后,进入 App 正常启动流程

流程图

优缺点

而计数器方法逻辑简单,与原有的代码耦合小。虽然有误报可能(在启动后立即被 kill 掉,误认为 crash),但是可以通过设置阈值来减小误报的误报率。

综上权衡,我们使用计时器方法检测连续闪退。

连续闪退修复

检测到连续闪退后,接下来要尝试对闪退进行修复,这里先分析可能的闪退原因,再结合微信读书的例子说明修复流程。

闪退原因

连续闪退,可能是 App 启动关键路径中执行了必 crash 的代码,原因可能有:

  1. 数据库损坏:在日常使用如异常退出、断电,或者错误的操作(参考:sqlite corruption causes)。

  2. 文件损坏:处理文件时如果没有 @try...catch,损坏文件会抛出 NSException 导致 crash

  3. 网络返回数据处理异常:比如预期返回数组,但实际返回了字典,对字典对象执行 -objectAtIndex 方法会产生 crash: unknow selector send to object;,或返回破损的 Tar 包,在解压失败导致 crash。

  4. 代码 bug:当必 crash 的代码出现在启动关键路径中,就会导致连续闪退。

针对 1,可以通过工具修复数据库,或者删除 DB。针对2,可以删除文件来进行修复。对于 3 和 4,我们需要具体地分析 crash 案例,通过 JSPatch 来进行修复。

微信读书的修复流程

为了应对上述导致连续闪退的原因,微信读书的修复流程为:

  1. 进入 didFinishLaunch 时检查是否有连续闪退,无则执行 5

  2. 弹 Toast 提示用户是否修复,轻触『修复』执行2,否则执行 5

  3. 尝试下载并执行 JSPatch 补丁

    这里是为了解决上述第4点 - 代码 bug 导致的闪退,使用 JSPatch [github]可以进行热修复。在 didFinishLaunching 时,会卡住界面发请求检查是否有可用的 JSPatch 脚本,如果有则加载执行,解决代码 bug 导致的闪退。

  4. 尝试删除 Documents / Library / Caches 目录下的所有文件

    这里直接删除了所有用户数据,适用于微信读书这种所有数据都在云端,删除后可以完全从云端恢复。如果你的 App 不属于这种场景,那么应该在 repairBlock 中自定义修复逻辑,比如:

    a. 不删除文件,只修复数据库
    b. 修复前把用户数据备份到云端
    c. 收集 crash 样本,查明原因,定制 JSPatch 修复补丁并下发

  5. 退出微信读书登录状态

  6. 进入原 didFinishLaunch

连续闪退检测 + 保护流程如图所示:

实现

检测和连续 crash 并修复需要修改原 -application:didFinishLaunchingWithOptions: 逻辑,有几种方法:

  1. 直接修改 -application:didFinishLaunchingWithOptions: 方法。

  2. 新建一个 SubAppDelegate 类来继承 AppDelegate,覆盖 -application:didFinishLaunchingWithOptions: 方法,然后把 main() 函数中的 AppDelegate 替换为 SubAppDelegate

  3. 新建一个 AppDelegate 扩展,然后用 method swizzle 的方法替换 -application:didFinishLaunchingWithOptions: 方法。

上述三种方案,对现有项目改动代价是 1 > 2 > 3。因此,我们使用对源码修改代价最小的方案 3 来替换 -application:didFinishLaunchingWithOptions:

检测的逻辑 GYBootingProtection 已经处理好,修复的处理预留了接口,可以由用户自定义,把自定义的修复流程传入 repairBlock 即可。

使用

引入项目

  1. 下载 (github) 源码 ,将 src 目录下所有文件拖拽到你的 Xcode 项目

  2. AppDelegate+GYBootingProtection.monBeforeBootingProtection 方法中添加检测前需要执行的代码,比如设置crash上报:

    - (void)onBeforeBootingProtection {
    [GYBootingProtection setLogger:^(NSString *msg) {
        // setup logger
        NSLog(@"%@", msg);
    }]; [GYBootingProtection setReportBlock:^(NSInteger crashCounts) {
        // setup crash report
    }];
    }
  3. onBootingProtection 方法中添加修复逻辑,比如删除文件:

     - (void)onBootingProtection {
        // 检查 JSPatch 更新
        ...
        // 删除 Documents Library Caches 目录下所有文件
        [GYBootingProtection deleteAllFilesUnderDocumentsLibraryCaches];
        ...
    }

    如需执行异步的修复逻辑,在 onBootingProtectionWithCompletion: 方法添加修复逻辑,并在完成修复后调用 completion :

     - (void)onBootingProtectionWithCompletion:(BoolCompletionBlock)completion {
           [self onBootingProtection];
        // 异步修复
           [self asyncRepairWithCompletion:^(void) {
            // 正常启动流程
               if (completion) completion();
           }];
    }

    测试与使用

  4. 首先制造连续闪退场景:

    启动后 5 秒内,双击 Home 通过上划手势 kill 掉 App,重复多次。(也可以在代码里人为制造crash)

  5. 当连续闪退超过 5 次时,会提示用户修复:

  6. 用户轻触修复,App 重置初始状态,连续闪退问题解决:

源码

https://github.com/liuslevis/GYBootingProtection

iOS 启动连续闪退保护方案的更多相关文章

  1. mac osx 启动wireshark闪退

    wireshark启动会提示安装x11 去x11地址安装后 启动还是闪退 原来是姿势不对 这样才行~~ 这一步 这个路径一定要对!路径一定要对!路径一定要对! 然后报错不用管它,如果没反应了,就继续等 ...

  2. 全网最详细的Windows里Anaconda-Navigator启动后闪退的解决方案(图文详解)

    不多说,直接上干货!  问题详情 点击 出现Anaconda-Navigator启动后闪退的现象. 或者 装过一次anaconda,貌似按了一个更新的键就打不开了.navigator这个打不开,会停留 ...

  3. 8. 启动Tomcat闪退无法启动原因解决

    原因可能是由于下面问题导致的: 1.Tomcat环境在不同电脑上引用的jdk路径不同 比如:在A电脑上Tomcat引用的jdk路径为:C:\Java\jdk1.6.0_10 而在B电脑上jdk的安装路 ...

  4. android app启动就闪退怎么办?

    开发过程中,如遇到android app启动就闪退,不要急,直接进入调试模式运行app,就会取得出错的原因. http://blog.sina.com.cn/s/blog_44fa172f0102wg ...

  5. MongoDB mongo.exe启动及闪退解决 转载

    转载自:http://blog.csdn.net/wyx_wx/article/details/76108662 启动: 进入MongoDB安装目录下的bin目录,启动mongod.exe 出现如图所 ...

  6. 启动扫描闪退,因为忘了在manifest里申请手机镜头使用许可了。

    启动扫描闪退,因为忘了在manifest里申请手机镜头使用许可了.

  7. iOS开发-闪退问题-解决之前上架的 App 在 iOS 9 会闪退问题

    最新更新:(2015.10.02) 开发环境: Delphi 10 Seattle OS X El Capitan v10.11 需使用下列 HotfixID: 30398, PAServer Hot ...

  8. 解决之前上架的 App 在 iOS 9 会闪退问题 (更新:已有 Hotfix)

    最新更新:(2015.10.02) 开发环境: Delphi 10 Seattle OS X El Capitan v10.11 需使用下列 HotfixID: 30398, PAServer Hot ...

  9. Tomcat学习笔记 - 错误日志 - Tomcat安装版安装后第二次启动后闪退(转)-- javac不是内部或外部命令 -- 配置java环境教程

    如果安装成功并且安装完成第一次启动是成功的,第二次就闪退的话,原因之一是没有配置java的环境.在网上找的配制方法有很多错误,测试javac命令时候会提示不是内部或外部命令,找到一个正确的教程.如下, ...

随机推荐

  1. this详解:JAVASCRIPT中的this到底是谁?

    语法 this 全局对象 在全局执行上下文(函数之外),this引用的是全局对象. console.log(this.document === document); // true // In web ...

  2. Apache与php的整合(经典版),花了一天去配置成功经验

    1.首先在官方下载php-7.0.7-Win32-VC14-x64.zip和httpd-2.4.20-win64-VC14.zip,也可以下载镜像版的apache:apache_2.4.4-x64-o ...

  3. 在CMD下用java命令出现“找不到或无法加载主类”问题

    解决思路: 从网上查找原因和解决方法,有提到环境变量classpath设置问题,但多次尝试问题依旧没有解决.然后使用java -cp %classpath; Hello执行,结果正确. 使用echo  ...

  4. Linux 环境下自动化测试工具,Redhat dogtail的安装

    dogtail基于Accessibility(a11y)的GUI图形界面测试工具和自动化框架可以与linux桌面应用程序进行交互操作. dogtail是用Python语言写的.dogtail的测试脚本 ...

  5. 【深入浅出jQuery】源码浅析2--使用技巧

    最近一直在研读 jQuery 源码,初看源码一头雾水毫无头绪,真正静下心来细看写的真是精妙,让你感叹代码之美. 其结构明晰,高内聚.低耦合,兼具优秀的性能与便利的扩展性,在浏览器的兼容性(功能缺陷.渐 ...

  6. 延长FLASH和EEPROM芯片写入次数的小方法

    开发电子产品时,常常需要断电后保存某些数据,这就需要使用 FLASH或EEPROM芯片,这两种芯片,可擦除的次数是有限制的,通常FLASH为10万次,EEPROM要多一点,为100万甚至1000万次. ...

  7. PhpStorm常用快捷键大全

    常用快捷键(keymaps:Default情况下)注意:部分快捷键,必须在没有更改快捷键的情况下才可以使用 功能键Esc键 返回编辑器窗口F1 打开在线帮助F2 (Shift+F2) 下/上高亮错误或 ...

  8. linux安装 Android Studio详细教程,支持性较差,需要安装最新底层库内核的linux

    安装 Android Studio详细教程 libc6-i386 lib32stdc++6 lib32gcc1 lib32ncurses5 lib32z1 jdk1.8.0_25 android-st ...

  9. poj 3007 Organize Your Train part II(静态字典树哈希)

    Time Limit: 1000MS Memory Limit: 65536K Total Submissions: 6700 Accepted: 1922 Description RJ Freigh ...

  10. oracle触发器与:new,:old的使用 --5

    :new --为一个引用最新的列值;:old --为一个引用以前的列值; 这两个变量只有在使用了关键字 "FOR EACH ROW"时才存在.且update语句两个都有,而inse ...