瞭解和分析iOS Crash Report

nimomeng發表於2019-02-09

圖片來自網路,侵權刪除

翻譯自蘋果官方文件:Understanding and Analyzing Application Crash Reports

nimo: 這篇長達1w多字的文章,大概前後翻譯了一個月,“寫”了三遍:第一遍是直譯,第二遍是把直譯改成程式設計師看著舒服的“行話”,第三遍是把原文裡說的過於抽象或者簡單的部分加上我的註解(大家看見所有以nimo開頭的部分)。 文章釋出後我才發現,這並不是針對iOS Crash report唯一的翻譯版本。哪篇翻譯的更好這個見仁見智,但我希望這篇是翻譯的最用心的版本。

當app發生crash時會產生crash report,這對我們定位crash的原因非常有幫助。這篇文件重點介紹瞭如何符號化、看懂並解析一篇crash Report。

nimo: 開篇給出了這個文件的三個階段,由淺入深為:

  1. 符號化,把不可讀的文件轉成可讀
  2. 看懂,意思就是知道文件裡哪個部分表達的什麼
  3. 解析,意思就是能從文件中定位問題,獲取解決問題的有價值的資訊。

介紹

當app發生crash時,系統會生成crash report並儲存在裝置上。crash report會描述app在何種情況之下被系統終止執行,一般情況下描述會包括完整的執行緒呼叫堆疊,這對app的除錯(和問題的定位)是非常有幫助的。所以你應當仔細研讀這些crash report,去了解你的app究竟發生的是哪種crash,並嘗試修復它們。

Crash Report,尤其是堆疊資訊,在被符號化之前是不可讀的。所謂符號化就是把記憶體地址用可讀的函式名和行數來替換。如果你不是從裝置直接獲取的crash日誌,而是通過Xcode的Device Window(即通過檢視操作而非手動命令列),它們會在幾秒之後自動被符號化。當然你也可以把.crash檔案加入到Xcode的Device Window並自行將它符號化。

Low Memory Report與其它crash report不同,它沒有堆疊資訊。當由於低記憶體而發生crash時,你必須反思你的記憶體使用模式和你針對低記憶體警告的應對方法。本文會提供給你幾個記憶體管理的參考實現,供你參考。

獲取Crash Report和Low Memory Report

如何除錯已經部署好的iOS Apps討論瞭如何從一個iOS裝置直接拿到crash report和low memory report。 App釋出指南裡的分析Crash Reports討論瞭如何檢視那些crash report,這些report既包含通過TestFlight下載的測試使用者處獲得,又包含通過App Store下載的正式使用者處獲得。

符號化一篇Crash report

符號化指的是一種手段,這種手段指的是把堆疊資訊(二進位制資訊)解釋成原始碼裡的方法名或者函式名,也就是所謂符號。只有符號化成功後,crash report才能幫助開發者定位問題。

注意:Low Memory Report不需要被符號化(因為沒有堆疊資訊)。 注意:在MacOS平臺上產生的crash report在生成的時候一般都會被完全符號化過或者半符號化過。因此本節指的符號化針對的是從iOS、watchOS乃至tvOS中提取出來的crash report。整體處理流程上,macOS的carsh report比較類似。

圖1:crash上報和符號化過程概述

  1. 編譯器在把你的原始碼轉換成機器碼的同時,也會生成一份對應的Debug符號表。Debug符號表其實是一個對映表,它把每一個藏在編譯好的binary資訊中的機器指令對映到生成它們的每一行原始碼中。通過build setting裡的Debug Information Format(DEBUG_INFORMATION_FORMAT),這些Debug符號表要麼被儲存在編譯好的binary資訊中,要麼單獨儲存在Debug Symbol檔案中(也就是dSYM檔案):一般來說,debug模式構建的app會把Debug符號表儲存在編譯好的binary資訊中,而release模式構建的app會把debug符號表儲存在dSYM檔案中以節省體積。 在每一次的編譯中,Debug符號表和app的binary資訊通過構建時的UUID相互關聯。每次構建時都會生成新的唯一的能夠標識那次構建的UUID,即便你用同樣的原始碼,通過同樣的編譯setting,UUID也不會相同。相應的,dSYM檔案也不能用於解析其它(UUID對應的)binary資訊,即便構建自於同一個原始碼。

nimo: 意思就是說,同一次構建,app+dSYM+UUID是一套的。如果這幾個檔案不屬於同一次構建,即便是相同的原始碼,互相之間在符號化這個事情上也無法互相工作。

  1. 當你為了分發app而選擇Archive(存檔)時,Xcode會把app的二進位制資訊和.dYSM檔案儲存在你的home資料夾下的某個地方。你可以在Xcode的Organizer裡面通過”Archived”選項找到所有你存檔過的app。 更多存檔app的細節,請點選官方文件-分發你的App一文。

注意:想要解析來自於測試、app review或者客戶的crash report,你需要保留分發出去的那些構建過的archive檔案。

  1. 如果你是通過App Store分發app或者是Test Flight分發的beta版本的app,你將在上傳archive到ITC(iTunes Connect)時看見一個“是否將dSYM一起上傳”的選項。在上傳對話方塊中,請勾選”在app中包含app符號表”。上傳你的dYSM檔案對於從TestFlight使用者和客戶以及願意分享診斷資訊的客戶那邊接收crash report是很有必要的。更多詳情請參考官方文件-分發你的App一文。

注意:接收自App Review的crash report是不會被符號化的,及時你再上傳你的app到ITC時勾選了包含dSYM檔案。任何來自於App Review的crash report都需要在Xcode裡做符號化。

  1. 當你的app 發生crash時,一個沒有被符號化的crash report會被建立並儲存在裝置上。

  2. 使用者可以通過除錯已部署的iOS APP裡提到的方法來直接從他們的裝置裡獲得crash report。如果你通過AdHoc或者企業證書分發app,這是你唯一能從使用者獲取crash report的方法。

  3. 從裝置上直接獲取的crash report是沒有被符號化的,你需要通過Xcode來符號化。Xcode會結合dSYM檔案和你app的二進位制資訊把堆疊裡的每一個地址對應到原始碼中。處理後的結果就是一個符號化過的crash report。

  4. 如果使用者願意和Apple共享診斷資訊,或者使用者通過TestFlight下載了你的beta版本app,那crash report會被上傳到App Store。

  5. App Store在符號化crash report後會把內部所有的crash reports做彙總並分組,這種聚合(相似crash report)的方法叫做crash聚類。

  6. 這些符號化後的crash report可以在你的Xcode的Crash Organizer中進行檢視。

Bitcode

Bitcode(位編碼)是一個編譯好的專案的中間表現形式。當你在允許bitcode的前提下Archive一個app時,編譯器會在二進位制中包含bitcode而不是機器碼。一旦binary資訊被上傳到App Store中,bitcode會被再次編譯成機器碼。也許App Store會在將來二次編譯bitcode,例如為提高編譯器效能而二次編譯等。不過這不重要,因為一切對你來說是透明的,也就不需要你來額外付出什麼。

圖2 BitCode編譯過程概覽

因為你的binary資訊的最終編譯結果是在App Store上體現的,因此你的Mac將不會包含那些需要對從App Review或者使用者的裝置那裡獲取到的Crash report所必須的符號化用的dSYM。

nimo:這裡原文很拗口,大概意思就是需要的東西都在App Store雲端,之後的操作會自動進行,見下文。

雖然當你Archive你的app時會建立dSYM檔案,但它們只能用在bitcode binary資訊中,並不能用於符號化crash report。 App Store允許你從Xcode或者ITC網站中下載這些隨著bitcode編譯而產生的dSYM檔案。 為了解析從App Review或者給你傳送crash report的使用者的crash report,你必須要下載這些dSYM檔案,這樣才能符號化crash report。 如果是從crash reporting service那裡接收crash report,符號化會自動完成。

注意:App Store上編譯的binary資訊和提交的原始檔案的UUID是不同的。

從Xcode下載dSYM檔案

  • 在Archives organizer,選擇你之前提交到App Store的Archive檔案
  • 選擇Download dSYM按鈕Archive

Xcode會下載dSYM檔案並且把他們插入到選擇的Archive中。

從ITC網站上下載dSYM檔案

  • 開啟App詳情頁面
  • 點選 Activity
  • 從所有的構建中,選擇一個版本
  • 點選 下載dSYM檔案的連結

把"隱藏的"符號名還原成原始名

當你把一個帶有bitcode的app上傳到App Store時,你也許在提交對話方塊中並沒有勾選“上傳你的app的符號表資訊以便從Apple那邊接收符號化過的 report”的選項。 當你選擇不傳送符號表資訊給Apple時,Xcode會在你傳送app到ITC之前用晦澀難懂的符號例如”_hidden#109”等來替換你的app裡的dSYM檔案。Xcode會建立一個原始符號和”隱藏”符號的對照表,並且將其儲存在Archive的app檔案中的一個bcsymbolmap檔案裡。每一個dSYM檔案都會有一個對應的bcsymbolmap檔案。

在符號化crash report之前,你需要把那些從ITC中下載下來的dSYM檔案中的晦澀資訊給解析一下。 如果你使用Xcode中的下載dSYM按鈕,這步解析會自動完成。但是,如果你通過ITC網站來下載dSYM的話,你需要開啟Terminal並且手動輸入下面的命令來做解析(把example的path資訊和dSYM資訊替換一下)

xcrun dsymutil -symbol-map ~/Library/Developer/Xcode/Archives/2017-11-23/MyGreatApp\ 11-23-17\,\ 12.00\ PM.xcarchive/BCSymbolMaps ~/Downloads/dSYMs/3B15C133-88AA-35B0-B8BA-84AF76826CE0.dSYM
複製程式碼

針對每一個dSYMs資料夾下的dSYM檔案都執行一次這條命令。

如何判斷Crash report是否已經符號化

一個crash report有可能未符號化,完全符號化,也有可能部分符號化。未符號化的crash report不會在堆疊資訊中包含方法名或者函式名。相反,你會在載入好的binary資訊中發現可執行的16進位制地址資訊。在完全符號化的crash report裡,堆疊中的每一行16進位制地址資訊都會被替換成對應的符號。在部分符號化的crash report中,只有一部分堆疊資訊被替換成相應的符號資訊。

顯然,你應當盡力去完全符號化你的crash report,因為那樣你才能夠獲得crash report裡最有價值的資訊。一個部分符號化的crash report也許包含了可以理解crash的資訊,這取決於crash的型別和哪一部分被成功符號化了。一個未符號化的crash report用處有限。

圖3 相同堆疊資訊下的不同程度的符號化

用Xcode符號化iOS的Crash report

一般來說,Xcode會自動嘗試符號化它所有的Crash report。所以你只需要把crash report加到Xcode Organizer就可以了。

Note:Xcode只認.crash字尾的crash report。如果你收到的crash report沒有字尾名或者字尾是txt,在執行下列步驟之前先把它改成.crash。

  • 把iOS裝置連線到你的Mac
  • 從Window選單欄選擇Devices
  • 在Devices左側,選擇一個裝置
  • 點選右邊在“Device Information“ 下面的 ”View Device Logs” 按鈕
  • 把你的Crash report拖拽到左側panel中
  • Xcode會自動符號化Crash report並且顯示結果

為了符號化一個Crash report,Xcode需要去定位如下資訊:

  • 崩潰的app的binary資訊以及dSYM檔案
  • 所有app關聯的自定義framework的binary資訊以及dSYM檔案。如果是從app構建出來的framework,它們的dYSM會隨著app的dSYM檔案一起拷貝到archive中。如果是第三方的framework,你需要去找作者要dYSM檔案。
  • 發生crash時app所依賴的OS的符號表資訊。這些符號表包含了特定OS版本(例如iOS9.3.3)上的framework所需除錯資訊。 OS 符號表的架構具有獨特性——一個64位的iOS裝置不會包含armv7的符號表。Xcode將要自動拷貝你連線到的特定版本的Mac的符號表。

在上述任何一處,如果沒有Xcode,你將無法符號化一個crash report,或者只能部分符號化一個crash report。

用atos符號化Crash report

atos命令可以把地址裡的數字替換成等價的符號。如果除錯符號資訊是完備的,則atos的輸出資訊將會包含檔名和對應的資源行數。atos命令可以被用來單獨符號化那些未符號化或者部分符號化過的crash report(中的堆疊資訊裡的地址)。 想要使用atos符號化crash report可以按如下方式操作:

  1. 找到你想要符號化的那一行,記下第二列的binary資訊名,以及第三列的地址。
  2. 從crash report底部的binary資訊名列表中找到那個名字,記下來架構名和載入的地址。

nimo: 例如在下圖裡,我們想符號化的部分就是0x00000001000effdc,binary資訊名是The Elements,底部能找到對應的名字的架構名稱是arm64,載入地址是0x1000e4000

圖4 在Crash report裡提取出使用atos所需要的資訊

  1. 定位二進位制對應的dSYM檔案。你可以用Splotlight,結合UUID,來尋找匹配的dSYM檔案。(請檢視相關章節。)dSYM是一個bundle,包含通過編譯器在build時編譯出來的DWARF除錯資訊(nimo: DWARF的可能的解釋是,Debugging With Attributed Record Formats,是一種除錯檔案結構標準,結構相當的複雜)。你在使用atos時必須提供這個檔案的路徑,而不是dSYM的bundle路徑。
  2. 有了上述資訊之後,你就可以把堆疊裡的地址通過atos命令來符號化了。你可以符號化多條地址,通過空格來進行區分。
atos -arch <Binary Architecture> -o <Path to dSYM file>/Contents/Resources/DWARF/<binary image name> -l <load address> <address to symbolicate>
複製程式碼

清單1 使用atos命令的樣例,以及結果輸出

$ atos -arch arm64 -o TheElements.app.dSYM/Contents/Resources/DWARF/TheElements -l 0x1000e4000 0x00000001000effdc
複製程式碼
-[AtomicElementViewController myTransitionDidStop:finished:context:]
複製程式碼

利用符號化排查問題

如果Xcode沒有完全符號化一個crash report,很可能是你的Mac丟失了app binary資訊對應的dSYM檔案,或者是丟了一個或多個app關聯的framework的dSYM檔案,也有可能在發生Crash時OS層面的app的裝置符號表丟失了。下列步驟顯示瞭如何使用Spotlight來判斷那些可以符號化對應堆疊地址資訊的dSYM檔案是否在你的Mac上。

圖5 定位一個二進位制映象

  1. 在Xcode無法符號化的堆疊裡找一行,注意第二列的binary資訊的名字。
  2. 在crash report的底部中的二進位制資訊列表裡找到那個名字。這個列表包含了每一個crash事故現場存在於程式裡的二進位制資訊的UUID。

nimo:本例中需要關注的binary資訊的名字是The Element,在底部列表中對應的二進位制資訊的UUID是77b672e2b9f53b0f95adbc4f68cb80d6

列表2 你可以用grep命令來快速找到二進位制資訊的列表資訊

$ grep --after-context=1000 "Binary Images:" <Path to Crash Report> | grep <Binary Name>
複製程式碼
  1. 把二進位制資訊的UUID按照 8-4-4-4-12格式(XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX)轉換成32個字元組成的字串。注意所有字母必須大寫。
  2. 用mdfind命令,結合”com_apple_xcode_dsym_uuids == ”(包含引號)來查詢UUID資訊。

列表3 使用mdfind命令來通過給定UUID查詢dSYM檔案。

$ mdfind "com_apple_xcode_dsym_uuids == <UUID>"
複製程式碼
  1. 如果spotlight找到了UUID對應的dSYM檔案,mdfind會把dSYM檔案和可能包含的歸檔檔案的路徑列印出來。如果一個UUID對應的dSYM檔案沒有找到,mdfind會直接退出。

如果spotlight找到了二進位制對應的dSYM檔案,但是Xcode沒有能結合二進位制資訊成功把地址符號化,那你應該上報一枚bug並且把crash report和對應的dSYM檔案一起附到bug report中。作為權宜之策,你可以手動用atos來對地址進行符號化。

如果spotlight沒有找到二進位制資訊對應的dSYM檔案,確保你還有app發生crash的那個版本的Xcode歸檔檔案,並且這個檔案存在於spotlight可以找到的某個地方。如果你的app是支援bitcode方式構建的,確保你已經從App Store下載了最終編譯版本的dSYM檔案。

如果你覺得你已經有了二進位制資訊對應的正確的dSYM檔案,那你可以用dwarfdump命令來列印對應的匹配UUID。你也可以用用dwarfdump命令來列印二進位制的UUID。

xcrun dwarfdump --uuid <Path to dSYM file>

注意:你必須儲存你最開始上傳到App Store的發生crash的app的歸檔檔案。dSYM檔案和app二進位制檔案是一一對應,且每次構建都不相同。即便通過相同的原始碼和配置,再執行一次構建,生成的dSYM檔案也無法和之前的crash report做符號化匹配。 如果你不在存有這個歸檔檔案,你應該重新提交一次有歸檔的新版本,以確保再發生crash的時候你可以符號化crash report。

分析Crash report

這段將會討論一篇標準crash report的各章節的含義。

Header

每一篇crash report都有一個header。

列表4 一篇crash report的header部分

Incident Identifier: B6FD1E8E-B39F-430B-ADDE-FC3A45ED368C
CrashReporter Key: f04e68ec62d3c66057628c9ba9839e30d55937dc
Hardware Model: iPad6,8
Process: TheElements [303]
Path: /private/var/containers/Bundle/Application/888C1FA2-3666-4AE2-9E8E-62E2F787DEC1/TheElements.app/TheElements
Identifier: com.example.apple-samplecode.TheElements
Version: 1.12
Code Type: ARM-64 (Native)
Role: Foreground
Parent Process: launchd [1]
Coalition: com.example.apple-samplecode.TheElements [402]
 
Date/Time: 2016-08-22 10:43:07.5806 -0700
Launch Time: 2016-08-22 10:43:01.0293 -0700
OS Version: iPhone OS 10.0 (14A5345a)
Report Version: 104
複製程式碼

大部分欄位的含義是不言自明的,但是有一些值得特別指出:

  • Incident Identifier: 一個crash report的唯一ID。兩個 report不會使用同一個Incident Identifier。
  • CrashReporter Key: 一個匿名的裝置相關ID。同一個裝置的兩篇crash report會有相同的CrashReporter Key。
  • Beta Identifier:一個整合了發生crash app的裝置和供應商資訊的ID。來自同一個供應商和裝置的兩篇report會包含相同的ID值。這個欄位只有當app通過TestFlight分發的時候出現,並且出現在應該出現Crash Reporter Key Field的地方。
  • Process:發生Crash時的程式名。這個和app資訊屬性列表裡的CFBundleExecutable Key中的值可以匹配上。
  • Version:發生crash的版本號。這個值可以關聯到發生 crash的app的CFBundleVersion 和 CFBundleVersionString上。
  • Code Type:發生crash的上下文所在架構環境。有ARM-64,ARM,X86-64和X86.
  • Role:在發生crash時程式的的task_role。

nimo:task_role的定義如下:

enum task_role {
	TASK_RENICED = -1,
	TASK_UNSPECIFIED = 0,
	TASK_FOREGROUND_APPLICATION,
	TASK_BACKGROUND_APPLICATION,
	TASK_CONTROL_APPLICATION,
	TASK_GRAPHICS_SERVER,
	TASK_THROTTLE_APPLICATION,
	TASK_NONUI_APPLICATION,
	TASK_DEFAULT_APPLICATION
};
複製程式碼
  • OS Version: OS version,包含發生crash時的所屬app的編譯碼。

異常資訊

遇到Objective-C/C++時不要懵(即便有些會導致Crash)。這章列出了Mach異常型別和相應的能提供crash的蛛絲馬跡的一些欄位資訊。當然,不是所有欄位都會出現在每一篇crash report裡。

列表5 由於uncaught Objective-C exception而導致的程式被停止的crash report的摘錄

Exception Type: EXC_CRASH (SIGABRT)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Exception Note: EXC_CORPSE_NOTIFY
Triggered by Thread: 0
複製程式碼

列表6 由於反向引用了一個NULL指標而造成程式被終止的crash report的摘錄

Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype: KERN_INVALID_ADDRESS at 0x0000000000000000
Termination Signal: Segmentation fault: 11
Termination Reason: Namespace SIGNAL, Code 0xb
Terminating Process: exc handler [0]
Triggered by Thread: 0
複製程式碼

可能出現在這一章節的某些欄位解讀如下。

  • Exception Codes: 和異常是有關的處理器指定資訊,這些資訊會被編碼成一個或者多個64位二進位制數字。一般來說,這個欄位不應該存在,因為crash report生成時會把exception code轉化成可讀的資訊並在其它欄位進行體現。
  • Exception Subtype:可讀的exception code的名稱。
  • Exception Message:從exception code中解析出來的附加的可讀資訊。
  • Exception Note:不特指某一種異常的額外資訊。如果這個欄位包含”SIMULATED”(不是Crash),則程式並沒有發生crash,而是在系統層面被kill掉了,比如看門狗機制。

nimo: 為了防止一個應用佔用過多的系統資源,蘋果工程師門設計了一個“看門狗”的機制。“看門狗”會監測應用的效能。如果超出了該場景所規定的執行時間,“看門狗”就會強制終結這個應用的程式。 開發者們在crashlog裡面,會看到諸如0x8badf00d這樣的錯誤程式碼(看起來很像bad food,看門狗吃到了壞的食物,不嗨森)。 看門狗觸發條件如下:

觸發時機 看門狗出動的時間
啟動 20秒
恢復執行 10秒
懸掛程式 10秒
退出應用 6秒
後臺執行 10分鐘
  • Termination Reason:當程式被終止時的原因及資訊。關鍵的資訊模組,不論是程式內還是程式外,當遇到一個致命錯誤(fatal error,例如bad code signature,缺失依賴庫,不恰當的訪問私有敏感資訊等)。MacOS Sierra,iOS 10, watch OS3和tvOS 10 已經採用新的架構去記錄這些錯誤資訊,所以這些系統之下的crash report會在Termination Reason這個欄位裡描述error message資訊。
  • Triggered by Thread:指出異常是在哪個執行緒發生的

接下來的章節會解釋常見的異常型別:

Bad Memory Access [EXC_BAD_ACCESS // SIGSEGV // SIGBUS]

程式試圖去訪問無效的記憶體空間,或者嘗試訪問的方法是不被允許的(例如給只讀的記憶體空間做寫操作)。在Exception Subtype欄位中如果出現kern_return_t的話,說明記憶體地址空間被不正確的訪問了。 這裡有幾個除錯bad memory crash的小貼士:

  • 如果 objc_msgSend 或者 objc_release出現在crash的執行緒的附近,則程式有可能嘗試去給一個被釋放的物件傳送訊息。你應當用Zombie instrument方式來執行profile,來更好地瞭解發生crash的原因。
  • 如果gpus_ReturnNotPermittedKillClient在近crash的執行緒附近,則程式有可能是嘗試在後臺通過OpenGL ES或者Metal來做渲染。可以參見 QA1766: How to fix OpenGL ES application crashes when moving to the background
  • 通過在執行你的app時勾選Address Sanitizer。address sanitizer會在編譯期間在記憶體訪問時新增額外的操作,當你的app執行,Xcode會在記憶體可能發生crash的時候給出提示資訊。

Abnormal Exit [EXC_CRASH // SIGABRT]

程式異常退出。這種異常最常見的原因在於uncaught Objective-C/C++ exception並且呼叫了abort()。 擴充套件App(nimo:App Extensions,例如輸入法)如果花了太多時間做初始化的話就會以這種異常退出(看門狗機制)。如果擴充套件程式由於在啟動時掛起進而被kill掉,那 report中的Exception Subtype欄位會寫LAUNCH_HANG。因為擴充套件App沒有main函式,所以任何情況下的在static constructors和+load方法裡的初始化時間都會體現在你的擴充套件或者依賴庫中。因此你應當儘可能的推遲這些邏輯。

Trace Trap [EXC_BREAKPOINT // SIGTRAP]

Abnormal Exit類似,這種異常是由於在特殊的節點加入debugger除錯節點的原因。你可以在你自己的程式碼裡通過使用__builtin_trap()函式來觸發這個異常。如果沒有debugger存在,則執行緒會被終止並生成一個crash report。 底層庫(例如libdispatch)會在遇到fatal錯誤的時候陷入這個困局。關於錯誤的相關資訊會在crash report的章節或者是裝置的的列印資訊裡找到。 Swift程式碼會在執行時的時候遇到下述問題時丟擲這種異常:

  • 一個non-optional的型別被賦予一個nil值
  • 一個失敗的強制轉換

遇到這種錯誤,查下堆疊資訊並想清楚是在哪裡遇到了未知情況(unexpected condition)。額外資訊也可能會在裝置的控制檯的日誌裡出現。你應當儘量修改你的程式碼,去優雅的處理這種執行時錯誤。例如,處理一個optional的值,通過可選繫結(Optional binding)而不是強制解包來獲得其值。

nimo: 可選繫結,就是類似如下語句的使用

if let actualValue = maybeHasValue(){
	foo(actualValue)
}
複製程式碼

Illegal Instruction [EXC_BAD_INSTRUCTION // SIGILL]

當嘗試去執行一個非法或者未定義的指令時會觸發該異常。有可能是因為執行緒在一個配置錯誤的函式指標的誤導下嘗試jump到一個無效地址。 在Intel處理器上,ud2操作碼會導致一個EXC_BAD_INSTRUCTIONY異常,但是這個通常用來做除錯用途。在Intel處理器上,Swift會在執行時碰到未知情況時被停止。 詳情參考Trace Trap。

Quit [SIGQUIT]

這個異常是由於其它程式擁有高優先順序且可以管理本程式(因此被高優先順序程式Kill掉)所導致。SIGQUIT不代表程式發生Crash了,但是它確實反映了某種不合理的行為。 iOS中,如果佔用了太長時間,鍵盤擴充套件程式會隨著宿主app被幹掉。因此,這種情況的異常下不太可能會在Crash report中出現合理可讀的異常程式碼。大概率是因為一些其它程式碼在啟動時佔用了太長時間但是在總時間限制前(看門狗的時間限制,見上文中的表格)成功結束了,但是執行邏輯在extension退出的時候被錯誤的執行了。你應該執行Profile,仔細分析一下extension的各部分消耗時間,把耗時較多的邏輯放到background或者推遲(推遲到extension載入完畢)。

Killed[SIGKILL]

程式收到系統指令被幹掉。請自行檢視Termination Reason來定位執行緒被幹掉的原因。 Termination Reason欄位會包含一個名稱空間和程式碼。以下程式碼只針對watchOS:

  • 程式碼0xc51bad01表示watchOS在後臺任務佔用了過多的cpu時間而導致watch app被幹掉。想要解決這個問題,優化後臺任務,提高CPU執行效率,或者減少後臺的任務執行數量。
  • 程式碼0xc51bad02表示在後臺的規定時間內沒有完成指定的後臺任務而導致watch app被幹掉。想要解決這個問題,需要當app在後臺執行時減少app的處理任務。
  • 程式碼0xc51bad03表示watch app沒有在規定時間內完成後臺任務,且系統一直非常忙以至於app無法獲取足夠的CPU時間來完成後臺任務。雖然一個app可以通過減少自身在後臺的執行任務來避免這個問題,但是0xc51bad03這個錯誤把矛頭指向了過高的系統負載,而非app本身有什麼問題。

Guarded Resource Violation [EXC_GUARD]

程式違規訪問了一個被保護的資源。系統庫會把特定的檔案描述符標記為被被保護,因此任何對這些檔案描述符的常規操作都會丟擲EXC_GUARD異常(nimo: 當系統想操作這些檔案描述符時,它們會用特殊的被授權過的私有API)。所以遇到諸如私自關閉掉系統開啟的檔案描述符之類的操作時您可以快速察覺。例如,如果一個app關閉掉了曾經支援Core Data 儲存的SQLite檔案的檔案描述符,你會發現Core Data過一會兒神祕crash。guard exception會在不久之後注意到並且讓他們更容易被debug。 更新版本的iOS crash report會在Exception Subtype和Exception Message欄位裡包含關於EXC_GUARD異常的可讀詳細資訊。在macOS或者是更老版本的iOS的crash report中,這條資訊會被加密成第一個Exception Code並以位資訊進行呈現,它可以被這麼解讀:

  • [63:61] - Guard Type:被保護的資源的型別。0x2值表示資源是一個檔案描述符。
  • [60:32] - Flavor:在何種情況之下出現的問題。 如果第一個(1<<0)bit被設值,則程式嘗試在一個被保護的檔案描述符上呼叫close()

如果第二個(1<<1)bit被設值,則程式嘗試在被保護的檔案描述符上用F_DUPFD 或 F_DUPFD_CLOEXEC呼叫dup(), dup2(), 或 fcntl()命令。

如果第三個(1<<2)bit被設值,則程式嘗試通過socket傳送一個被保護的檔案描述符。

如果第五個(1<<4)bit被設值,則程式嘗試寫一個被保護的檔案描述符。

  • [31:0] - File Descriptor:程式嘗試修改被保護的檔案描述符。

Resource Limit [EXC_RESOURCE]

程式的資源超過限定閾值。這條推送是OS發出的,表示程式佔有了太多的資源。準確的資源列在了Exception Subtype的欄位裡。如果Exception Note欄位包含了NON-FATAL CONDITION(非嚴重錯誤),則即便是生成了crash report,程式也不會被kill掉。

  • 如果EXCEPTION SUBTYPE裡出現MEMORY則暗示了程式佔用已經超過系統限制。如果之後出現由於系統佔用過多程式被Kill,可能和這有關。
  • 如果EXCEPTION SUBTYPE裡出現WAKEUP則暗示執行緒每秒被程式喚醒太多次了,進而導致CPU被頻繁喚醒並且造成電量損耗。 通常,這種事發生在程式間通訊(通過peformSelector:onThread:或者dispatch_async),而且會遠比預想的發生的更頻繁。因為發生這種異常的通訊被觸發的如此頻繁,所以很多後臺程式會出現彼此高度雷同的堆疊資訊——恰恰暗示了它們是從哪兒來的。

Other Exception Types

有些crash report可能會出現無名的Exception Type,這時候在這個欄位上會出現純16進位制值(例如00000020)。如果你收到這樣的crash report,直接去Exception Code檢視更多資訊。

  • 如果Exception Code是0xbaaaaaad則說明此條logs是系統堆疊快照,並非crash report。可以通過同時按(手機)側邊按鈕和音量鍵來記錄堆疊快照。通常情況下,這些logs是使用者無意中生成的,並非表示錯誤。
  • 如果Exception Code是0xbad22222表示一個VoIP應用因為頻繁暫停被iOS系統終止掉。
  • 如果Exception Code是0x8badf00d(讀起來像badfood)則說明一個應用因為觸發了看門狗機制被iOS系統終止掉,有可能是應用花了太長時間啟動,終止,或者是響應系統事件。一種常見原因是在主執行緒上做網路同步邏輯。不論Thread0上(也就是主執行緒)想做什麼(重要的事),都應該轉移到後臺執行緒,或者換一種方式觸發,這樣它才不會阻塞主執行緒。
  • 如果Exception Code是0xc00010ff則說明app因為環境過熱(的事件)被iOS系統幹掉了。這個也許是和發生crash的特定裝置有關,或者是和它所在的環境有關。如果想知道更多高效執行app的tips,請參考WWDC的文章: iOS Performance and Power Optimization with Instruments
  • 如果Exception Code是0xdead10cc(讀起來像deadlock)則說明一個應用被系統終止掉,原因是在應用掛起時拿到了檔案鎖或者sqlite資料庫所長期不釋放直到被凍結。如果你的app在掛起時拿到了檔案鎖或者sqlite資料庫鎖,它必須請求額外的後臺執行時間(request additional background execution time )並在被掛起前完成解鎖操作。
  • 如果Exception Code是0x2bad45ec則說明app因為違規操作(安全違規)被iOS系統終止。終止描述會寫:“程式被查到在安全模式進行非安全操作”,暗示app嘗試在禁止螢幕繪製的時候繪製螢幕,例如當螢幕鎖定時。使用者可能會忽略這種異常,尤其當螢幕是關閉的或者當這種終止發生時正好鎖屏。

Note:通過App Switcher(就是雙擊home鍵出現的那個介面)並不會生成crash report。一旦app進入掛起狀態,被iOS在任何時間終止掉都是合理的,因此這時候不會生成crash report。

額外的診斷資訊

本章節包含終止相關的額外診斷資訊,包括:

  • 應用的具體資訊:在程式被終止前捕捉到的框架錯誤資訊
  • 核心資訊:關於程式碼簽名問題的細節
  • Dyld (動態連結庫)錯誤資訊:被動態連結器提交的錯誤資訊

從macOS Sierra, iOS 10, watchOS 3, 和 tvOS 10開始,大部分這種資訊都在Exception InformationTermination Reason欄位下了。 你應當閱讀本章節來更好的明白當程式被終止的時候發生了什麼。

表7:一段因為找不到連結庫而導致程式被終止的crash report的摘錄

Dyld Error Message:
Dyld Message: Library not loaded: @rpath/MyCustomFramework.framework/MyCustomFramework
  Referenced from: /private/var/containers/Bundle/Application/CD9DB546-A449-41A4-A08B-87E57EE11354/TheElements.app/TheElements
  Reason: no suitable image found.
複製程式碼

表8:一段因為沒能快速載入初始view controller而導致程式被終止的crash report的摘錄

Application Specific Information:
com.example.apple-samplecode.TheElements failed to scene-create after 19.81s (launch took 0.19s of total time limit 20.00s)
Elapsed total CPU time (seconds): 7.690 (user 7.690, system 0.000), 19% CPU
Elapsed application CPU time (seconds): 0.697, 2% CPU
複製程式碼

堆疊資訊

一個crash report最有意思的部分一定是每個執行緒在被終止時的堆疊資訊。這些資訊和你在debug時看到的很類似。

列表9:一個完全符號化的crash report的堆疊部分摘錄

Thread 0 name: Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0   TheElements                   	0x000000010006bc20 -[AtomicElementViewController myTransitionDidStop:finished:context:] (AtomicElementViewController.m:203)
1   UIKit                         	0x0000000194cef0f0 -[UIViewAnimationState sendDelegateAnimationDidStop:finished:] + 312
2   UIKit                         	0x0000000194ceef30 -[UIViewAnimationState animationDidStop:finished:] + 160
3   QuartzCore                    	0x0000000192178404 CA::Layer::run_animation_callbacks(void*) + 260
4   libdispatch.dylib             	0x000000018dd6d1c0 _dispatch_client_callout + 16
5   libdispatch.dylib             	0x000000018dd71d6c _dispatch_main_queue_callback_4CF + 1000
6   CoreFoundation                	0x000000018ee91f2c __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 12
7   CoreFoundation                	0x000000018ee8fb18 __CFRunLoopRun + 1660
8   CoreFoundation                	0x000000018edbe048 CFRunLoopRunSpecific + 444
9   GraphicsServices              	0x000000019083f198 GSEventRunModal + 180
10  UIKit                         	0x0000000194d21bd0 -[UIApplication _run] + 684
11  UIKit                         	0x0000000194d1c908 UIApplicationMain + 208
12  TheElements                   	0x00000001000653c0 main (main.m:55)
13  libdyld.dylib                 	0x000000018dda05b8 start + 4

Thread 1:
0   libsystem_kernel.dylib        	0x000000018deb2a88 __workq_kernreturn + 8
1   libsystem_pthread.dylib       	0x000000018df75188 _pthread_wqthread + 968
2   libsystem_pthread.dylib       	0x000000018df74db4 start_wqthread + 4

...
複製程式碼

第一行列出了當前的執行緒號,以及當前的執行佇列的id。其餘各行列出來每一個堆疊中堆疊片段資訊,從左到右分別是:

  • 堆疊片段號。堆疊的展示順序會和呼叫順序一致,片段0是在程式被終止時執行的函式。片段1是呼叫片段0的函式,以此類推。
  • 在堆疊片段中駐留的執行函式的名稱
  • 片段0代表機器指令在被終止的生活所在的地址。其它片段表示如果片段0執行完成之後下一個執行的片段地址
  • 在一個符號化的crash report中,代表在堆疊片段中的函式名稱

異常

Objective-C中的異常通常用來表明在執行時發生的程式碼錯誤,例如越界訪問陣列,或者更改immutable的物件,沒有實現protocol中必須實現的方法,或者給接收者無法識別的物件傳送資訊。

Note:給之前已經釋放的物件傳送訊息會引發NSInvalidArgumentException異常進而crash,而非記憶體訪問違規。這會在新的變數正好佔據了之前釋放變數所在記憶體時。如果你的app因為NSInvalidArgumentException發生crash(在堆疊資訊中檢視[NSObject(NSObject) doesNotRecognizeSelector:]),考慮通過 Zombies instrument 來profiling你的應用,來排除剛才提到的記憶體管理問題。

如果異常沒有被捕捉到,他會被一個叫uncaught exception方法所攔截。預設的uncaught exception的日誌會顯示到裝置的控制檯,之後會終止程式。異常堆疊資訊會在生成的crash report的上一個異常堆疊(Last Exception Backtrace)下,就像列表10所寫。異常訊息會被crash report忽略。如果你收到了一個帶有上一個異常堆疊(Last Exception Backtrace)的crash report,你應當去獲取原始裝置並獲取其控制檯日誌資訊,來更好的瞭解發生crash的原因。

List10:發生了上一個異常堆疊(Last Exception Backtrace)的未符號化crash report摘錄

Last Exception Backtrace:

(0x18eee41c0 0x18d91c55c 0x18eee3e88 0x18f8ea1a0 0x195013fe4 0x1951acf20 0x18ee03dc4 0x1951ab8f4 0x195458128 0x19545fa20 0x19545fc7c 0x19545ff70 0x194de4594 0x194e94e8c 0x194f47d8c 0x194f39b40 0x194ca92ac 0x18ee917dc 0x18ee8f40c 0x18ee8f89c 0x18edbe048 0x19083f198 0x194d21bd0 0x194d1c908 0x1000ad45c 0x18dda05b8)
複製程式碼

一個只包含16進位制資訊的有Last Exception Backtrace資訊的crash日誌必須被符號化,以獲取有價值的堆疊資訊,就像列表11所寫。

列表11:一個包含Last Exception Backtrace資訊的符號化的crash report。這個異常出現在載入app的storyboard時,需要響應的IBOutlet的對應元素丟失了。

Last Exception Backtrace:

0   CoreFoundation                	0x18eee41c0 __exceptionPreprocess + 124
1   libobjc.A.dylib               	0x18d91c55c objc_exception_throw + 56
2   CoreFoundation                	0x18eee3e88 -[NSException raise] + 12
3   Foundation                    	0x18f8ea1a0 -[NSObject(NSKeyValueCoding) setValue:forKey:] + 272
4   UIKit                         	0x195013fe4 -[UIViewController setValue:forKey:] + 104
5   UIKit                         	0x1951acf20 -[UIRuntimeOutletConnection connect] + 124
6   CoreFoundation                	0x18ee03dc4 -[NSArray makeObjectsPerformSelector:] + 232
7   UIKit                         	0x1951ab8f4 -[UINib instantiateWithOwner:options:] + 1756
8   UIKit                         	0x195458128 -[UIStoryboard instantiateViewControllerWithIdentifier:] + 196
9   UIKit                         	0x19545fa20 -[UIStoryboardSegueTemplate instantiateOrFindDestinationViewControllerWithSender:] + 92
10  UIKit                         	0x19545fc7c -[UIStoryboardSegueTemplate _perform:] + 56
11  UIKit                         	0x19545ff70 -[UIStoryboardSegueTemplate perform:] + 160
12  UIKit                         	0x194de4594 -[UITableView _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:] + 1352
13  UIKit                         	0x194e94e8c -[UITableView _userSelectRowAtPendingSelectionIndexPath:] + 268
14  UIKit                         	0x194f47d8c _runAfterCACommitDeferredBlocks + 292
15  UIKit                         	0x194f39b40 _cleanUpAfterCAFlushAndRunDeferredBlocks + 560
16  UIKit                         	0x194ca92ac _afterCACommitHandler + 168
17  CoreFoundation                	0x18ee917dc __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 32
18  CoreFoundation                	0x18ee8f40c __CFRunLoopDoObservers + 372
19  CoreFoundation                	0x18ee8f89c __CFRunLoopRun + 1024
20  CoreFoundation                	0x18edbe048 CFRunLoopRunSpecific + 444
21  GraphicsServices              	0x19083f198 GSEventRunModal + 180
22  UIKit                         	0x194d21bd0 -[UIApplication _run] + 684
23  UIKit                         	0x194d1c908 UIApplicationMain + 208
24  TheElements                   	0x1000ad45c main (main.m:55)
複製程式碼

如果你發現本應該被捕捉的異常並沒有被捕捉到,請確定您沒有在building應用或者library時新增了-no_compact_unwind標籤。

64位IOS用了zero-cost的異常實現機制。在zero-cost系統裡,每一個函式都有一個額外的資料,它會描述如果一個異常在跨函式範圍內實現,該如何展開相應的堆疊資訊。如果一個異常發生在多個堆疊但是沒有可展開的資料,那麼異常處理函式自然無法跟蹤並記錄。也許在堆疊很上層的地方有異常處理函式,但是如果那裡沒有一個片段的可展開資訊,沒辦法從發生異常的地方到那裡。指定了-no_compact_unwind標籤表明你那些程式碼沒有可展開資訊,所以你不能跨越函式丟擲異常(也就是說無法通過別的函式捕捉當前函式的異常)。

Thread State(執行緒狀態)

這章列出了crash執行緒的執行緒狀態。這裡列出了註冊過的值。在你讀一個crash report的時候,瞭解執行緒狀態並非必須,但是如果你想更好地瞭解crash的細節,這也許會起一些幫助。

列表12:ARM64裝置的crash report的一段Thread State摘錄

Thread 0 crashed with ARM Thread State (64-bit):

    x0: 0x0000000000000000   x1: 0x000000019ff776c8   x2: 0x0000000000000000   x3: 0x000000019ff776c8
    x4: 0x0000000000000000   x5: 0x0000000000000001   x6: 0x0000000000000000   x7: 0x00000000000000d0
    x8: 0x0000000100023920   x9: 0x0000000000000000  x10: 0x000000019ff7dff0  x11: 0x0000000c0000000f
   x12: 0x000000013e63b4d0  x13: 0x000001a19ff75009  x14: 0x0000000000000000  x15: 0x0000000000000000
   x16: 0x0000000187b3f1b9  x17: 0x0000000181ed488c  x18: 0x0000000000000000  x19: 0x000000013e544780
   x20: 0x000000013fa49560  x21: 0x0000000000000001  x22: 0x000000013fc05f90  x23: 0x000000010001e069
   x24: 0x0000000000000000  x25: 0x000000019ff776c8  x26: 0xee009ec07c8c24c7  x27: 0x0000000000000020
   x28: 0x0000000000000000  fp: 0x000000016fdf29e0   lr: 0x0000000100017cf8
    sp: 0x000000016fdf2980   pc: 0x0000000100017d14 cpsr: 0x60000000
複製程式碼

Binary Images

這一章列出了在程式被終止時載入在程式中的二進位制檔案(binary images)。

列表13:一段crash report的完整二進位制檔案摘錄

Binary Images:

0x100060000 - 0x100073fff TheElements arm64 <2defdbea0c873a52afa458cf14cd169e> /var/containers/Bundle/Application/888C1FA2-3666-4AE2-9E8E-62E2F787DEC1/TheElements.app/TheElements

...
複製程式碼

每一行都包含了一個二進位制檔案的以下細節資訊:

  • 在程式內的二進位制檔案的地址空間
  • 一段二進位制的名稱或者bundle id(僅針對macOS)。一個MacOS的crash report,如果二進位制時OS的一部分,會在前面加上a
  • (僅針對macOS)二進位制的短版本(short version)和bundle版本,通過破折號來分割。
  • (僅針對iOS)二進位制檔案的架構名。一個二進位制可能包含多個分片,每一個架構它都支援。其中只有一個可以被載入到程式中。
  • 一個可以唯一標示二進位制檔案的id,即UUID。這個值會隨每一次構建而發生變化,並且它會用來定位需要符號化時的dSYM檔案。
  • 磁碟上二進位制檔案的path。

讀懂低記憶體 report(Low Memory Reports)

當系統檢測到記憶體不足時,iOS系統裡的虛擬記憶體系統會協同各應用來做記憶體釋放。每個執行著的應用都會接收到系統發來低記憶體推送(Low-memory notification),要求釋放記憶體空間,從而達到減少整體記憶體消耗的目的。如果記憶體壓力依然存在,系統可能會終止後臺程式以減輕記憶體壓力。如果(整體)記憶體釋放夠了,你的應用將可以繼續執行;不然,你的應用會被iOS終止,因為可供你的應用執行的記憶體不夠,這時候會生成一個低記憶體 report(Low-Memory Report)並儲存在你的裝置中。 低記憶體 report的格式和其它crash report略有不同,它沒有應用的堆疊資訊。一個低記憶體 report的Header會和crash report的header有些類似。緊接著Header的時各個欄位的系統級別的記憶體統計資訊。記錄下頁大小(Page Size)欄位。每一個程式的記憶體佔用大小是根據記憶體的頁的數量來 report的。 一個低記憶體 report最重要的部分是程式表格。這個表格列出了所有的執行程式,包括系統在生成低記憶體 report時的守護程式。如果一個程式被”遺棄”了,會在[原因]一列附上具體的原因。一個程式可能被遺棄的原因有:

  • [per-process-limit]:程式佔用超過了它的最大記憶體值。每一個程式在常駐記憶體上的限制是早已經由系統為每個應用分配好了的。超過這個限制會導致程式被系統幹掉。

注意:擴充套件程式(nimo: Extension app, 例如輸入法等)的最大記憶體值更少。一些技術,例如地圖檢視和SpriteKit,佔用非常多的基礎記憶體,因此不適合用在擴充套件程式裡。

  • [vm-pageshortage]/[vm-thrashing]/[vm]:由於系統記憶體壓力被幹掉。
  • [vnode-limit]: 開啟太多檔案了。

注意:系統會盡量避免在vnodes已經枯竭的時候幹掉高頻app。因此你的應用如果在後臺,即便並沒有佔用什麼vnode,而有可能被殺掉。

  • [highwater]:一個系統守護程式超過過了它的記憶體佔用高水位(就是已經很危險了)。
  • [jettisoned]:程式因為其它不可描述的原因被殺掉。

如果你沒有在你的應用或者擴充套件程式裡看到原因,那crash的原因就不是低記憶體壓力。仔細檢視一下.crash檔案(在之前章節裡有寫)。

當你發現一個低記憶體crash,與其去擔心哪一部分的程式碼出現問題,還不如仔細審視一下自己的記憶體使用習慣和針對低記憶體告警(low-memory warning)的處理措施。Locating Memory Issues in Your App 列出瞭如何使用Leaks Instrument工具來檢查記憶體洩漏,和如何使用Allocations Instrument的Mark Heap 功能來避免記憶體浪費。 Memory Usage Performance Guidelines 討論瞭如何處理接受到低記憶體告警的問題,以及如何高效使用記憶體。當然,也推薦你去看下2010年的WWDC中的 Advanced Memory Analysis with Instruments 那一章節。

重要:Leaks和Allocation工具不能檢測所有的記憶體使用情況。你需要和VM Tracker工具一起執行(包含在Allocation工具裡)來檢視你的記憶體執行。預設VM Tracker是不可用的。如果想通過VM Tracker來profile你的應用,點選instrument工具,選中”Automatic Snapshotting”標籤或者手動點選”Snapshot Now”按鈕。

相關文件

如果想檢視如何使用Zombies模板工具來修復記憶體釋放的crash,可以檢視Eradicating Zombies with the Zombies Trace Template 。 如果想檢視應用歸檔的資訊,請參考 App Distribution Guide 。 如果想了解關於crash logs的解讀,請參考Understanding Crash Reports on iPhone OS WWDC 2010 Session

相關文章