0xdead10cc问题调研

近期我们新做了一个需求,其中一个实现是需要在启动和使用这个功能时,从网络获取大量数据,然后将数据保存到数据库中。后续操作我们均从数据库获取。但是该需求上线后,Xcode中反馈了很多0xdead10cc问题。因此本篇文章主要是介绍对0xdead10cc的了解。

确定原因

首先先放出一张Xcode中Crash的堆栈信息
crash
我们看到Crash信息中我们可以明确地看到Crash的原因:
Termination Reason: Namespace RUNNINGBOARD, Code 0xdead10cc
我们先来看下这到底是一个什么错误。

1
2
3
4
5
6
7
8
9
> The exception code

0xdead10cc (pronounced “dead lock”). The operating system terminated the app
because it held on to a file lock or SQLite database lock during suspension.
Request additional background execution time on the main thread with
beginBackgroundTask(withName:expirationHandler:). Make this request well before
starting to write to the file in order to complete those operations and
relinquish the lock before the app suspends. In an app extension,
use beginActivity(options:reason:) to mange this work.

通过上面的描述我们可以知道,0xdead10cc这个错误出现的原因是:在App被挂起期间,操作系统发现App依然持有一个文件锁或者数据库锁

而从堆栈信息中,我们可以看到在崩溃前的确是在执行数据库相关操作,那么这个Crash崩溃的原因应该是在挂起期间依然持有了数据库锁(进行数据库操作)

知识储备

首先我们应该明白什么是suspension状态,下面是官网截图

应用状态分析

1
2
3
4
5
6
7
8
9
10
The following figure shows the state transitions for scenes. When the user or 
system requests a new scene for your app, UIKit creates it and puts it in the
unattached state. User-requested scenes move quickly to the foreground, where they
appear onscreen. A system-requested scene typically moves to the background so
that it can process an event. For example, the system might launch the scene in
the background to process a location event. When the user dismisses your app's
UI, UIKit moves the associated scene to the background state and eventually to
the suspended state. UIKit can disconnect a background or suspended scene at any
time to reclaim its resources, returning that scene to the unattached state.

我们只需要重点看下这句话即可:
When the user your app's UI, UIKit moves the associated scene to the background state and eventually to the suspended state.

当用户离开了你的APP,UIKit就会将相关的场景切换到后台状态,最终进入到挂起状态
对于当应用即进入后台时应该进行什么操作,苹果官方也给我们提供了详细的描述: Preparing Your UI to Run in the Background

我们都知道在我们进入后台时,App的代码是可以继续执行的,但是挂起态明显不可以,那么从后台状态到挂起态,我们有多久的时间可以继续执行任务呢?

这一点官方文档并没有明确的解释,只是提示我们要As Soon As Possible,因此对于没有申请后台执行时间的APP来说,我们最好尽可能快的在APP进入后台时取消可能会继续持有的资源。当然如果APP需要可以申请额外的后台运行时间,但是时间也是有限的。

这里也找到了Finite-Length Tasks in background iOS swift供大家参考。

那么系统是否有提供通知,我们可以动态监听应用进入了挂起态呢?
在了解挂起态后,我们应该可以猜得到系统并不会给我们提供这个通知,因为在这个阶段App是不允许执行任何操作的,因此即使系统为开发者提供了这个通知,App也是无法监听并执行相应操作的。

这里找到了Execution States for a Swift iOS App这篇文章,其中介绍了Not RunningSuspendedBackgroundInactiveInactive 这几种状态有兴趣的可以了解下。

场景分析

经过上面的介绍,我们已经有了这方面的只是储备,那么在回到我们的crash上。下面是我对这次崩溃场景的猜测:App进入启动后,进入我们从接口获取数据保存到数据库的操作,对于数据较多的用户这里可能会持续时间较久,在这个过程中用户进入了后台,接口获取和保存数据库操作持续进行中,在某个接口请求完成后刚好到达临界时间,系统即将把App置为挂起状态。此时接口完成后的数据库操作,刚好导致了App持有数据库锁,因此系统将App强杀,并Report了上面的Crash

系统为何要这么做

因为我们的sqlite db是App Group共用(包括Extension)且文件是位于shared container,这就意味着文件会同时被App和Extension访问,假设App写操作在执行中途被suspend暂停,Extension唤醒后也对同一个App执行写操作,那么当App被重新唤醒继续之前的写操作时,写操作和db文件就会处于一个不可预知的状态,有可能造成写操作失败或者db文件损坏,所以系统选择了强杀App

解决方案

了解了问题出现的场景,我们就比较好去模拟场景,但是App从后台状态进入挂起态的时间是不确定的,因此在模拟该状态时时间点是无法确认的,因此尝试了多次后仍无法复现。

因此对于这个问题,我们只能从业务的角度,当App进入后台时我们将对应的数据库操作暂停,当应用再次回到前台时,通过判断之前是否有因进入后台而暂停的数据库操作,如果有,那么我们重新进行该次的接口+数据库写操作。

不过这样做副作用也很明显:在App进入后台之后,我们无法继续进行接口和网络请求了!这对于一些要求数据完全获取才可以展示的页面是十分不友好的。

期望

大家如果有什么比较好的复现这类问题的方法,可以留言提供感激不尽,如果有比价好的解决方案就更好了😄

参考文章

How can I tell if my app has suspended?

About the Background Execution Sequence

Springboard crashing when adding a lot of triggers to UNUserNotificationCenter

iOS App 后台 Crash 调查

The Curious Case of the Core Data Crash

0xdead10cc prevention

Understanding the Exception Types in a Crash Report