0xdead10cc問題調研

語言: CN / TW / HK

近期我們新做了一個需求,其中一個實現是需要在啟動和使用這個功能時,從網路獲取大量資料,然後將資料儲存到資料庫中。後續操作我們均從資料庫獲取。但是該需求上線後,Xcode中反饋了很多 0xdead10cc 問題。因此本篇文章主要是介紹對 0xdead10cc 的瞭解。

確定原因

首先先放出一張Xcode中Crash的堆疊資訊

我們看到Crash資訊中我們可以明確地看到Crash的原因:

Termination Reason: Namespace RUNNINGBOARD, Code 0xdead10cc

我們先來看下這到底是一個什麼錯誤。

> 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狀態,下面是官網截圖

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進入後臺之後,我們無法繼續進行介面和網路請求了!這對於一些要求資料完全獲取才可以展示的頁面是十分不友好的。