iOS App 后台 Crash 调查

Apple 一直在逐步放大 App 后台运行的权限,到今天为止,已知的 iOS App 后台运行场景有:

Background Task

通常情况下,App 一旦进入后台,只有数秒的时间继续执行代码,之后就会被系统 Suspend。除非 App 显示的调用 beginBackgroundTaskWithExpirationHandler API,能延迟 App 在后台运行代码的时间,iOS 7 之前是 10 分钟,iOS 7 之后缩减至 3 分钟。一旦时间期限到,系统依旧会 Suspend 进程。

Suspend 进程意味着系统会中断 App 的一切代码逻辑,比如所有线程会暂停,内存读写会暂停,文件访问也会中断,比如 sqlite 读写操作即使写至一半,也会进入暂停状态。理解这一点,能更好的分析我们 App 在后台的行为。

对于大部分 App 来说,都会利用 Background Task 机制来延长后台运行时间,不过一旦 App 体量增大,业务场景增加,我们很难保证所有的代码在进入后台之后,都会被封装到 Background Task 之中执行,很难确保 Background Task 一旦 expire,App 会主动暂停所有内部逻辑,做一个 iOS 系统遵法守纪的好公民。

想象下,App 在 Background Task 里发送一个网络请求,由于延迟较大,后台运行 3 分钟即将超时 response 才回来,进而触发一些列代码逻辑,比如写数据库,此时由于后台时间额度已用完,iOS 系统会强行终止进程,App 的状态会变得不可预知。

虽然 Apple 建议 App 在进入后台后应该运行尽可能少的关键任务,但这种规范对于业务类型复杂的 App 很难遵循,我们能做的,就是在进入后台 3 分钟之内,尽可能完成核心任务,而且不触发新的代码流程。

Background Mode

开启这种模式的 App 可以一直在后台运行,像早期的 VOIP 类应用,导航类 App,支持后台播放音乐的 App 以及一些蓝牙类 App 都需要这种 Background Mode 来正常运行核心功能。开启这种模式的副作用是审核严苛,而且耗电严重,用户会比较敏感。

Background Fetch

这是 Apple 提供的,在系统指定的时间段唤醒 App 并执行少量逻辑的机制,限制较多,唤醒并不可靠,比如被用户强杀的 App 无法唤醒,比如有些用户为了省电考虑,会在系统设置里强行关闭所有的 background fetch。

Silent Push

这是通过 APN 里设置特定字段来唤醒 App 的机制,同 Background fetch 限制较多,在强杀时无法唤醒。

PushKit

PushKit 是用来替代 VOIP 后台运行模式的新机制,在收到语音或者视频电话时,可以通过 PushKit 的通知来唤醒 App,从而避免 App 在后台一直运行。通过 PushKit 唤醒来唤醒 App 非常可靠,基本上具备 VOIP 功能的 App 都会使用这一机制来提升 App 体验。

PushKit 的后台运行模式非常有趣,据我观察,一般情况下,后台唤醒后有 30 秒(并不精准,只是大致接近)的运行时间,如果唤醒后开启 Background Task 能将运行时间延长至 40 秒。

如果 30 秒内连续收到两个 PushKit 通知,那么 App 总共的后台运行时间加起来还是 30 秒,如果 30 秒之后被 Suspend,再收到第二个 Push,那么又可以获得额外的 30 秒。有点类似 Session 的概念,每次收到 Push 可以启动一个 30 秒的 Session,一个 Session 可以处理多个 Push,一旦 Session 结束就被系统 Suspend,再次收到 Push 时可以启动一个新的 Session。

Background Crash 调查

以上是 iOS App 后台运行的现有机制的简单介绍,最近在调查一个 background crash,需要用到上述的后台运行机制。

用户抱怨 App 的 cold start 非常频繁,导致耗电严重。

起初怀疑是 background crash,问题是,开发人员检查了后台日志,发现并无相关的 crash 记录。那么有可能是 BOOM,但 BOOM 发生的概率一般来说比较低。

后来在拿到问题手机之后,查看 analytics 日志,发现如下 crash report:

很显然 App 是在后台时被系统强杀,原因是在后台运行时还持有某个 db 文件的锁。code 为 0xdead10cc,搜索 Apple 官方文档:

The exception code 0xdead10cc indicates that an application has been terminated by iOS because it held on to a system resource (like the address book database) while running in the background.

原因有了大致方向,接下来时分析具体 Crash 场景。通过仔细查看 App 的运行日志之后,分析出以下行为:

App 在后台首先通过 PushKit 被唤醒,获得 30 秒的运行时间,在 30 秒即将结束的时候,收到新的 PushKit 通知,进而触发一些列流程,比如 sqlite 读写,而因为 30 秒后台时间已用完,系统会强行 suspend App 进程。

系统在 suspend 进程之前,会多做一道检查,如果 App 此时持有 sqlite db 文件的锁(比如正在进行写操作),而且所访问的 sqlite db 文件是位于 shared container 目录下,系统会强杀进程,并生成一个类似上面的 Crash Report。

系统为什么要这么做?很简单,如果 sqlite db 文件是位于 shared container,意味着该文件会同时被 App 和 Extension 访问,假设 App 的写操作在执行中途被 suspend 暂停,Extension 唤醒后也对同一个 db 文件执行写操作,那么当 App 被重新唤醒继续之前写操作时,写操作和 db 文件就会处于一个不可预知的状态,有可能造成写操作失败或者 db 文件损坏,所以系统选择强杀 App。

即使明白了 Crash 过程,要修复却并不简单。原因就像文章开头提到的,一个大体量的 App 在进入后台之后,其代码的执行场景会变得十分复杂,我们并没有简单的机制来确保,进入后台后的流程都能在系统限制的时间内完成,只能通过日志一个个模块排查,简化 App 进入后台之后的行为。

其他收获

另外值得一提的是,从这次 Crash 调查可以看出,并不是所有的 Crash 都能通过 App 内部的 Crash 收集工具获得日志,已知的至少有这几类 Crash 是无法被 App 捕获的:

  • 前台主线程卡死,App 被 Watchdog 强杀
  • App 在前台或者后台使用过多的内存,被系统强杀,分别为 FOOM 和 BOOM。
  • App 在后台被 suspend 之后,由于违反 Apple 的某个 policy,而被系统强杀。

所以你的 App 即使有了成熟的 Crash 采集工具和后台,有时候还是需要登陆 Itunes Connect 后台去查看下日志,或者通过 Xcode 直接查看,因为有些系统强杀并不会通知 App,只有系统能生成和获取日志。这些日记也可以通过用户手机查看,位于 Settings->Privacy->Analytics->Analytics Data。

#


Hosted by Coding Pages