iOS App 後臺任務的坑

ios8988發表於2018-09-12

大多數 iOS App 在進入後臺之後都會將一些關鍵任務封裝到 Background Task 裡,否則程式在若干秒之後就會被系統 Suspend。啟動 Background Task 之後,可以獲得 3 分鐘繼續執行程式碼的時間。

最近在調查 Messenger 的 Background Crash 問題,最後都追蹤到和 Background Task 相關,和大家分享下一些要點。

Crash 訊號

一般 App 都有自己的 crash 日誌採集工具,這類工具一般有三個問題。第一是在工具啟動之前的 crash 日誌無法捕捉,第二是如果 App 啟動閃退日誌無法上傳,第三是一些特殊場景的系統強殺無法捕捉 crash 訊號。

解決第一個問題,只要將工具的執行時間儘可能提前,或者確保之前的程式碼及可能簡單可靠。

解決第二個問題,可以採用我之前分享過的,使用 NSURLSession 的 background mode。

解決第三個問題,需要依賴於 Apple 自己的 crash 訊號,這也是很多開發團隊所忽視的一點。

Apple 也有自己的 crash 日誌採集,不過基於使用者隱私的考慮,這個 crash 日誌並不可靠,主要存在以下幾方面的缺陷:

  • 使用者需同意上傳並分享資料,據聞,同意比例不足 20%,所以無法準確確定某個 crash 的實際影響面。

  • crash 日誌工具簡陋,通過 Xcode -> Organizer 開啟,選中 App 就能從 Apple 後臺下載某個版本的 crash 日誌,無法通過某個條件做篩選,比如你不能過濾出所有 SIGKILL 的日誌。

  • 日誌不全,Apple 按照自己的規則呈現 crash 樣本,一個 App 實際線上的 crash 非常之多,但 Apple 列出的 crash 樣本只有數十個,規則不明。

  • crash 日誌只儲存一週,一週重新整理一次,所有比較明智的做法是寫個指令碼同步下來,上傳到自己的後臺。

Background Task 花式 crash

Background Task 的 API 及其簡單,begin 和 end 之間的程式碼全部進入 Background Task 的範疇。但簡單的程式碼隱藏著不小的風險,下面列出三個比較容易出現的 crash。而且這三個 crash 都是客戶端自帶的 crash 採集工具無法捕捉的,只能通過 Apple 的 crash 日誌獲得訊號。原因很簡單,這些 crash 發生的時候 app 一般處於 suspend 狀態,根本沒有機會執行任何程式碼,系統直接傳送 SIGKILL 訊號後就將 app 強殺,並生成一個系統日誌,一個只能 Apple 訪問的日誌,還得使用者先同意上傳分享。

0xdead10cc

這個 crash 日誌一般長這樣: IOS,馬甲包,低要求,內容開發沒有限制,報酬豐厚,實力誠信 Q:782675105

Exception Type:  EXC_CRASH (SIGKILL)

Exception Codes: 0x0000000000000000, 0x0000000000000000

Exception Note:  EXC_CORPSE_NOTIFY

Termination Reason: Namespace SPRINGBOARD, Code 0xdead10cc

Termination Description: SPRINGBOARD, com.xxx.xxx was task-suspended with locked system file

原因我之前介紹過,當你的 App 有 Extension,而且 Extension 存在和 Host App 共享資料的需求,一般做法會將 db 檔案放入 shared container 目錄下,此時你的 App 就有大概率會發生這種 crash。

App 進入後臺執行 Background Task,end 之後 App 被系統 suspend,如果 suspend 之後還存在任何訪問 db 的操作,此時 App 會立馬被系統強殺,這是 Apple 出於保護資料庫檔案的完整的考慮。

所以正確的做法是將所有有可能在 App 進入後臺之後,還會發生的 db 操作統統封入 Background Task,以確保安全。這個程式碼寫在 db layer 可能更加合適。

而且 Apple 推薦當你想啟動 Background Task 的時候,其實並不需要考慮當前 App 是出於 foreground 還是 background,即使 App 在前臺啟動 Background Task,也並不會佔用進入後臺之後 3 分鐘額度,所以放心大膽的把關鍵程式碼放進 Background Task 吧。

0xbada5e47

當你聽從了上面的建議,大大方方的把儘可能多的關鍵程式碼封入 Background Task 後,那麼你可能會遇到下面的 crash:

Exception Type:  EXC_CRASH (SIGKILL)

Exception Codes: 0x0000000000000000, 0x0000000000000000

Exception Note:  EXC_CORPSE_NOTIFY

Termination Reason: Namespace ASSERTIOND, Code 0xbada5e47

同理也是和 Background Task 相關,原因是 Apple 認為你啟動了過多的 Background Task,所以要殺掉。多少算多呢?幾十個不多,當前的 threshold 是 1000 個,超過 1000 個才會強殺。如果你的 Background Task 封裝發生在 db layer,出現大量資料過來需要儲存或讀取的時候,還是有可能會 hit 這個 limit。

另一個 0xbada5e47 的可能原因是,Background Task 在超時之後會呼叫 expiry handler,無論你有多少個 Background Task,所有 expiry handler 執行的時間不能超過若干秒,一旦超過也會被槍殺。所以在 expiry handler 裡面切忌有任何比如 disk io 的耗時操作。

0x8badf00d

說到 0x8badf00d,大家都很熟悉了,當你的主執行緒卡住的時間太長,系統的 Watchdog 會將你的 App 強殺,並生成一個帶有 0x8badf00d 的 crash 日誌。

Background Task 其實也可以 0x8badf00d 的,比如:

Exception Type:  EXC_CRASH (SIGKILL)

Exception Codes: 0x0000000000000000, 0x0000000000000000

Exception Note:  EXC_CORPSE_NOTIFY

Termination Reason: Namespace SPRINGBOARD, Code 0x8badf00d

當你的程式碼邏輯會產生 leaked Background Task 時,就會出現上面的系統強殺 crash 日誌了,什麼是 leaked Background Task 呢?看程式碼:

- (void)startBgTask
{
 self.bgTaskID = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
   NSLog(@"Expired: %lu", (unsigned long)self.bgTaskID);
   [[UIApplication sharedApplication] endBackgroundTask:self.bgTaskID];
 }];
}

- (void)endBgTask
{
 [[UIApplication sharedApplication] endBackgroundTask:self.bgTaskID];
}

 

上面的程式碼如果 startBgTask 執行兩次,就一定會出現 leaked Background Task,因為 self.bgTaskID 第二次會被賦予一個新的 ID,之前的 task ID 就丟失了,無法正確呼叫 end。

那怎麼判斷 0x8badf00d 到底是主執行緒卡死,還是出現了 leaked Background Task ?很簡單,看主執行緒的 stack,如果長這樣:

Thread 0 Crashed:0   

libsystem_kernel.dylib        0x000000018472be08 0x18472b000 + 35921   

libsystem_kernel.dylib        0x000000018472bc80 0x18472b000 + 32002   

CoreFoundation                0x0000000184c6ee40 0x184b81000 + 9744003   

CoreFoundation                0x0000000184c6c908 0x184b81000 + 9648724   

CoreFoundation                0x0000000184b8cda8 0x184b81000 + 485525   

GraphicsServices              0x0000000186b6f020 0x186b64000 + 450886   

UIKit                         0x000000018eb6d78c 0x18e850000 + 32664447   

Messenger                     0x0000000103015ee4 0x102ff8000 + 1225968   

libdyld.dylib                 0x000000018461dfc0 0x18461d000 + 4032

這個 stack 很經典,經常會看到,不需要 symbolicate 也能知道是幹啥,這是 UI 執行緒 runloop 處於 idle 狀態的 stack,在等待 kernel 的 message。表示 UI 執行緒此時處於閒置狀態,這種狀態下的系統強殺大概率是由於 leaked Background Task 導致的。

善用裝置本地的 crash 日誌

當使用者的手機遇到 crash,而你既無法重現又在後臺找不到 crash 日誌的時候,此時你最大的希望就是手機本地的 crash 日誌了。

本地日誌位於 Settings -> Privacy -> Analytics -> Analytics Data。開啟看下,說不定你所開發的 App 的 crash 日誌,我手機上微信和支付寶都有好些日誌。

日誌排序先是按照 App 的名稱,再按日誌發生的日期。

如果調查記憶體使用過多的 crash,可以檢視 JetsamEvent-xxx 開頭的日誌。

如果想知道 App 發生 crash 前系統有哪些異常日誌,需要首先在裝置上安裝一個 loggingiOS.mobileconfig 的檔案,這個檔案基本上就是讓使用者授權給你記錄系統行為,使用者在遇到 crash 的時候,同時按下兩個音量鍵 + 電源鍵,鬆手震動之後,系統會將過去一段時間的關鍵日誌記錄下來,對於分析一些疑難雜症很有幫助,這種日誌一般為 sysdiagnose_xxx 開頭。

安裝上述 loggingiOS.mobileconfig 檔案之後,還有另外一個好處,Apple 會記錄更多而且更詳細的 crash 日誌了,因為使用者授權過,所以 Apple 可以大膽施為了。這類日誌的檔名一般為:stacks + appName – date.ips。

如果使用者的裝置能重現你所調查的問題,還有另一個簡單高效的辦法,將手機 usb 連線 mac,然後啟動 mac 上的 Console App,就能直觀的看到所有系統關鍵日誌了,比如網路異常日誌可以檢視 nsurlsessiond,定位異常日誌檢視 locationd,Background Task 異常日誌可以檢視 assertiond,也可以直接按照你 app 的程式名進行過濾,檢視生命週期以及被強殺的原因。

總結

以上是最近調查 Background Task crash 的一些知識點分享,希望對大家有所幫助。

相關文章