iOS Crash的捕获知识

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 ( + 72)
2   UIKit                           0x0000000192c4db48 0x192c05000 + 297800 ( + 312)
3   UIKit                           0x0000000192c4d988 0x192c05000 + 297352 ( + 160)
4   QuartzCore                      0x00000001900d6404 0x18ffc5000 + 1119236 ( + 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(发送到邮箱/上传自己服务器)
  • 自定义捕获+堆栈符号化

你可能感兴趣的:(iOS Crash的捕获知识)