1. crash的类型
crash一般产生自 iOS 的微内核 Mach,然后在 BSD 层转换成 UNIX SIGABRT 信号,以标准 POSIX 信号的形式提供给用户。NSException 是使用者在处理 App 逻辑时,用编程的方法抛出。
- Mach 异常:EXC_CRASH
- UNIX 信号:SIGABRT
- NSException 异常:应用层,通过 NSUncaughtExceptionHandler 捕获
2. crash的捕获的方式
Mach 异常捕获。如果想要做mach 异常捕获,需要注册一个异常端口,这个异常端口会对当前任务的所有线程有效,如果想要针对单个线程,可以通过 thread_set_exception_ports注册自己的异常端口,发生异常时,首先会将异常抛给线程的异常端口,然后尝试抛给任务的异常端口,当我们捕获异常时,就可以做一些自己的工作,比如,当前堆栈收集等。
-
Unix 信号捕获。对于Mach 异常,操作系统会将其转换为对应的 Unix信号,所以如果你对Mach不熟悉的话,也可以通过注册signalHandler的方式来做信号异常。
signal(SIGABRT, SignalExceptionHandler) signal(SIGSEGV, SignalExceptionHandler) signal(SIGBUS, SignalExceptionHandler) signal(SIGTRAP, SignalExceptionHandler) signal(SIGILL, SignalExceptionHandler)
NSException 捕获
对于NSException异常,也比较容易处理,通过注册NSUncaughtExceptionHandler捕获异常信息即可,将拿到的NSException细节写入Crash日志,上传到后台做数据分析
NSSetUncaughtExceptionHandler(UncaughtExceptionHandler)
多个 Crash 收集框架存在时,往往会存在冲突。
不管是对于 Signal 捕获还是 NSException 捕获都会存在 handler 覆盖的问题,正确的做法应该是先判断是否有前者已经注册了 handler,如果有则应该把这个 handler 保存下来,在自己处理完自己的 handler 之后,再把这个 handler 抛出去,供前面的注册者处理。
资料: 漫谈iOS Crash收集框架
4.堆栈符号解析
堆栈符号化还原有三种常见的方法:
- symbolicatecrash
- mac 下的 atos 工具
- 通过 dSYM 文件提取地址和符号的对应关系,进行符号还原
// 未符号化前
Thread 0 name: Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0 libobjc.A.dylib 0x000000018b816f30 0x18b7fc000 + 110384 (objc_msgSend + 16)
1 UIKit 0x0000000192e0a79c 0x192c05000 + 2119580 (<redacted> + 72)
2 UIKit 0x0000000192c4db48 0x192c05000 + 297800 (<redacted> + 312)
3 UIKit 0x0000000192c4d988 0x192c05000 + 297352 (<redacted> + 160)
4 QuartzCore 0x00000001900d6404 0x18ffc5000 + 1119236 (<redacted> + 260)
// 符号化后
Thread 0 name: Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0 libobjc.A.dylib 0x000000018b816f30 objc_msgSend + 16
1 UIKit 0x0000000192e0a79c -[UISearchDisplayController _sendDelegateDidBeginDidEndSearch] + 72
2 UIKit 0x0000000192c4db48 -[UIViewAnimationState sendDelegateAnimationDidStop:finished:] + 312
3 UIKit 0x0000000192c4d988 -[UIViewAnimationState animationDidStop:finished:] + 160
4 QuartzCore 0x00000001900d6404 CA::Layer::run_animation_callbacks(void*) + 260
Xcode的Organizer内置了symbolicatecrash,所以我们才可以直接看到符号化的崩溃堆栈日志
资料:实战iOS崩溃堆栈的符号化解析
轻量级的堆栈采集框架:BSBacktraceLogger
5.启动连续闪退
-
闪退原因
1. 数据库损坏:在日常使用如异常退出、断电,或者错误的操作(参考:sqlite corruption causes)。 2. 文件损坏:处理文件时如果没有 @try...catch,损坏文件会抛出 NSException 导致 crash 3. 网络返回数据处理异常:比如预期返回数组,但实际返回了字典,对字典对象执行 -objectAtIndex 方法会产生 crash: unknow selector send to object;,或返回破损的 Tar 包,在解压失败导致 crash。 4. 代码 bug:当必 crash 的代码出现在启动关键路径中,就会导致连续闪退。 5. 针对 1,可以通过工具修复数据库,或者删除 DB。针对2,可以删除文件来进行修复。对于 3 和 4,我们需要具体地分析 crash 案例,通过 JSPatch 来进行修复。
-
计时器方法
1. 维护一个计数变量,用于表示连续闪退的次数 2. 在启动 application:didFinishLaunchingWithOptions: 后使计数加一 3. 接着使用 dispatch_after 方法在 5s 后清零计数,如果 App 活不过 5 秒计数就不会被清零 4. 如果发现计数变量 > n,表明 App 连续 n 次连续闪退,启动保护流程,重置计数。 5. 当保护流程完成后,进入 App 正常启动流程
时间数组比对
在本地保存一个 App 每次启动时间、闪退时间、手动关闭时间的时间数组,然后在 App 启动时根据分析各个时间戳判断是否存在连续闪退(当闪退时间减去启动时间小于阈值 5 秒时,则认为是启动闪退)
1. App 每次启动时,记录当前时间 launchTs,写入时间数组;
2. App 每次启动时,通过 crash 采集库,获取上次 crash report 的时间戳 crashTs,写入时间数组;
3. App 在接收到 UIApplicationWillTerminateNotification 通知时,记录当前时间戳 terminateTs,写入时间数组。注意,之所以要记录 terminateTs,是为了排除一种特殊情况,即用户启动 App 之后立即手动 kill app。
资料:
iOS 启动连续闪退保护方案
连续启动 crash 自修复技术实现与原理解析
两种 App 启动连续闪退检测策略
6.crash收集方式
- 第三方平台:Fabric、友盟、腾讯 Bugly等,数据会上传到这些平台
- 第三方工具: KSCrash、plcrashreporter等,可自行处理收集的crash(发送到邮箱/上传自己服务器)
- 自定义捕获+堆栈符号化