iOS 崩溃千奇百怪,如何全面监控

常见的崩溃情况

  • 数组越界:在取数据索引时越界,App 会发生崩溃。还有一种情况,就是给数组添加了 nil 会崩溃。
  • 多线程问题:在子线程中进行 UI 更新可能会发生崩溃。多个线程进行数据的读取操作,因为处理时机不一致,比如有一个线程在置空数据的同时另一个线程在读取这个数据,可能会出现崩溃情况。
  • 主线程无响应:如果主线程超过系统规定的时间无响应,就会被 Watchdog 杀掉。这时,崩溃问题对应的异常编码是 0x8badf00d。关于这个异常编码,我还会在后文和你说明。
  • 野指针:指针指向一个已删除的对象访问内存区域时,会出现野指针崩溃。野指针问题是需要我们重点关注的,因为它是导致 App 崩溃的最常见,也是最难定位的一种情况。

常见的崩溃情况分类

信号可捕获的分类

  • KVO问题
  • NSNotification线程问题
  • 数组越界
  • 野指针等

信号不可捕获的分类

  • 后台任务超时
  • 内存打爆
  • 主线程卡顿超阈值等

信号可捕获的崩溃信息收集

崩溃监控工具

将崩溃时获取到的崩溃信息保存本地,下次打开App是上传崩溃信息

信号不可捕获的崩溃信息收集

后台容易崩溃的原因是什么?

先介绍下 iOS 后台保活的 5 种方式:Background Mode、Background Fetch、Silent Push、PushKit、Background Task。

  • 使用 Background Mode 方式的话,App Store 在审核时会提高对 App 的要求。通常情况下,只有那些地图、音乐播放、VoIP 类的 App 才能通过审核。
  • Background Fetch 方式的唤醒时间不稳定,而且用户可以在系统里设置关闭这种方式,导致它的使用场景很少。
  • Silent Push 是推送的一种,会在后台唤起 App 30 秒。它的优先级很低,会调用 application:didReceiveRemoteNotifiacation:fetchCompletionHandler: 这个 delegate,和普通的 remote push notification 推送调用的 delegate 是一样的。
  • PushKit 后台唤醒 App 后能够保活 30 秒。它主要用于提升 VoIP 应用的体验。
  • Background Task 方式,是使用最多的。App 退后台后,默认都会使用这种方式。

在你的程序退到后台以后,只有几秒钟的时间可以执行代码,接下来就会被系统挂起。进程挂起后所有线程都会暂停,不管这个线程是文件读写还是内存读写都会被暂停。但是,数据读写过程无法暂停只能被中断,中断时数据读写异常而且容易损坏文件,所以系统会选择主动杀掉 App 进程。

而 Background Task 这种方式,就是系统提供了 beginBackgroundTaskWithExpirationHandler 方法来延长后台执行时间,可以解决你退后台后还需要一些时间去处理一些任务的诉求。

如何避免后台崩溃呢?

App 退后台后,如果执行时间过长就会导致被系统杀掉。那么,如果我们要想避免这种崩溃发生的话,就需要严格控制后台数据的读写操作。比如,你可以先判断需要处理的数据的大小,如果数据过大,也就是在后台限制时间内或延长后台执行时间后也处理不完的话,可以考虑在程序下次启动或后台唤醒时再进行处理。

同时,App 退后台后,这种由于在规定时间内没有处理完而被系统强制杀掉的崩溃,是无法通过信号被捕获到的。这也说明了,随着团队规模扩大,要想保证 App 高可用的话,后台崩溃的监控就尤为重要了。

怎么去收集退后台后超过保活阈值而导致信号捕获不到的那些崩溃信息呢?

采用 Background Task 方式时,我们可以根据 beginBackgroundTaskWithExpirationHandler 会让后台保活 3 分钟这个阈值,先设置一个计时器,在接近 3 分钟时判断后台程序是否还在执行。如果还在执行的话,我们就可以判断该程序即将后台崩溃,进行上报、记录,以达到监控的效果。

还有哪些信号捕获不到的崩溃情况?怎样监控其他无法通过信号捕获的崩溃信息?

其他捕获不到的崩溃情况还有很多,主要就是内存打爆和主线程卡顿时间超过阈值被 watchdog 杀掉这两种情况。

其实,监控这两类崩溃的思路和监控后台崩溃类似,我们都先要找到它们的阈值,然后在临近阈值时还在执行的后台程序,判断为将要崩溃,收集信息并上报。

采集到崩溃信息后如何分析并解决崩溃问题呢?

我们采集到的崩溃日志,主要包含的信息为:进程信息、基本信息、异常信息、线程回溯。

  • 进程信息:崩溃进程的相关信息,比如崩溃报告唯一标识符、唯一键值、设备标识;

  • 基本信息:崩溃发生的日期、iOS 版本;

  • 异常信息:异常类型、异常编码、异常的线程;

  • 线程回溯:崩溃时的方法调用栈。

通常情况下,我们分析崩溃日志时最先看的是异常信息,分析出问题的是哪个线程,在线程回溯里找到那个线程;然后,分析方法调用栈,符号化后的方法调用栈可以完整地看到方法调用的过程,从而知道问题发生在哪个方法的调用上。

异常编码

完整的崩溃日志里,除了线程方法调用栈还有异常编码。异常编码,就在异常信息里。

一些被系统杀掉的情况,我们可以通过异常编码来分析。你可以在维基百科上,查看完整的异常编码。这里列出了 44 种异常编码,但常见的就是如下三种:

  • 0x8badf00d,表示 App 在一定时间内无响应而被 watchdog 杀掉的情况。
  • 0xdeadfa11,表示 App 被用户强制退出。
  • 0xc00010ff,表示 App 因为运行造成设备温度太高而被杀掉。

0x8badf00d 这种情况是出现最多的。当出现被 watchdog 杀掉的情况时,我们就可以把范围控制在主线程被卡的情况。

0xdeadfa11 的情况,是用户的主动行为,我们不用太关注。

0xc00010ff 这种情况,就要对每个线程 CPU 进行针对性的检查和优化。