WWDC 2018:理解崩潰以及崩潰日誌

知識小集發表於2018-06-11

WWDC 2018 Session 414: Understanding Crashes and Crash Logs

檢視更多 WWDC 18 相關文章請前往 老司機x知識小集xSwiftGG WWDC 18 專題目錄

作者簡介:@Vong,目前就職於美拍,喜歡折騰~

人非聖賢,孰能無過。每個人在寫程式碼的時候,或多或少都會犯錯,那麼如何除錯、找出問題所在呢?讓我們跟隨蘋果工程師一起了解一下崩潰是如何產生以及如何解決它們的吧。

1. 基礎知識

崩潰是什麼?崩潰是當應用想要做某件事的時候,被意外終止。

1.1 崩潰為什麼會發生

主要是以下幾方面原因

  • CPU 無法執行的程式碼。
  • 被作業系統“強殺”,系統為了使用者體驗,會強制終止掉那些卡頓時間過長或者記憶體消耗過高的應用。
  • 程式語言為了防止錯誤發生而觸發的崩潰,如 NSArray 或者 Swift.Array 越界
  • 開發者為了防止錯誤發生而觸發的崩潰,比如一些非空判斷的斷言

1.2 崩潰長什麼樣子

1.2.1 偵錯程式裡

當我們連線著 Xcode 進行除錯的時候,遇到崩潰,大概長這個樣子。

WWDC 2018:理解崩潰以及崩潰日誌

當連著偵錯程式的時候,我們能夠拿到崩潰現場的一些呼叫棧以及對應的方法,當沒有連著偵錯程式的時候,系統會將崩潰日誌儲存到磁碟當中。

1.2.2 崩潰日誌裡

通常情況下,release 模式的應用的崩潰日誌是沒有符號化的,日誌內記錄的都是地址。我們可以通過 Xcode 來將崩潰日誌進行符號化,解析出對應檔名、方法名以及對應崩潰在第幾行。

1.3 獲取崩潰日誌

獲取崩潰日誌的方式很多,我們先來了解一下如何通過 Xcode Organizer 來獲取從 TestFlightApp Store 下載的應用的崩潰日誌。

1.3.1 Organizer Window

先來看一下下面這張圖:

WWDC 2018:理解崩潰以及崩潰日誌

下面數字 1~6 分別代表圖中標註的 1~6

  • 1.可以看到所有平臺釋出在 App Store 或者 TestFlight 上的應用。
  • 2.崩潰日誌列表,可以看到對應影響的裝置數以及對應的平臺、擴充套件(extension),如圖中藍色框標註的位置。
  • 3.崩潰所在呼叫棧及崩潰位置的高亮。
  • 4.在對應工程中開啟崩潰所在的檔案,並跳轉到指定位置,方便追蹤問題。
  • 5.最近資料分析,包含系統和機型兩個維度。
  • 6.在崩潰數較多時,支援翻頁。

PS:上面6個只是簡單介紹了一下主題部分,剩餘的可以自行探索使用。比如搜尋、對單個日誌做一些筆記、以及將已修復的崩潰標記為已解決等等。

那麼如何才能在 Organizer 中獲取對應的崩潰日誌呢?很簡單,只需要做到下面幾步

    1. Xcode 中登入已付費的開發者帳號。
    1. 上傳應用到 App StoreTestFlight 時,一併上傳符號檔案。
    1. 開啟 Xcode Organizer 視窗,選中 Crashes tab(快捷鍵:Cmd+Shift+6)。

1.3.2 Devices Window

WWDC 2018:理解崩潰以及崩潰日誌

連線上裝置,開啟 Xcode,使用快捷鍵 Cmd+Shift+2 來開啟 Devices Window,選中對應裝置,然後選擇 View Device Logs,即可檢視當前裝置磁碟上的所有崩潰檔案,找到應用對應的日誌即可展開分析。

有些時候,獲取到的崩潰日誌並沒有符號化。這個時候需要自己做一些額外操作,這裡可以參考我之前在知識小集分享過的一個小 tip——iOS快速解析崩潰日誌

1.3.3 其它途徑

  • Xcode 的自動化測試(得到的是已符號化的日誌)
  • Mac 自帶的 Console 應用,獲取 Mac 或者模擬器的崩潰日誌
  • iOS裝置可通過這種操作獲取,開啟【設定】->【隱私】->【分析】->【分析資料】拿到對應的未符號化的崩潰日誌,然後通過系統自帶的分享即可傳輸到對應的裝置上進行分析。

1.4 符號化最佳實踐

  • 上傳應用的符號檔案,以便蘋果後臺可以直接符號化崩潰日誌,最終得以在 Xcode OrganizerCrashestab 中呈現。
  • 保留應用歸檔檔案,以便做本地符號化,只要有歸檔檔案在,Xcode 會自動進行符號化。
  • Xcode OrganizerArchivetab 為已開啟 bitcode 的應用下載 dSYM 檔案。

2. 分析奔潰日誌

2.1 崩潰日誌的組成

  • 崩潰摘要,主要記錄一些基本資訊,比如機型、系統版本、崩潰時間等
  • 崩潰原因
  • 崩潰資訊(這一部分在真機上處於隱私原因,一般都是不可見的,在模擬器和 MacOS 上可見)
  • 崩潰執行緒的呼叫棧
  • 崩潰發生時,其它執行緒的呼叫棧
  • 暫存器狀態
  • 已載入的可執行二進位制檔案

2.2 如何分析

首先從崩潰原因中的崩潰型別開始

WWDC 2018:理解崩潰以及崩潰日誌

如上圖的崩潰型別為 EXC_BAD_INSTRUCTION,它代表 CPU 嘗試在執行一段不存在或無效的程式碼,而導致進行被“殺死”。

WWDC 2018:理解崩潰以及崩潰日誌

然後我們可以找到崩潰執行緒的呼叫棧的前幾行,結合崩潰資訊(如果有的話)進一步分析。找到崩潰棧中第一處二進位制名為應用名稱所在那一行,進到對應檔案對應的程式碼行數進行檢視(如上圖中標紅的那一行),然後進一步分析。上圖中的崩潰可以很明顯看出其原因是對 nil 進行了強制解包。

2.3 斷言和先決條件導致的崩潰

斷言和先決條件的意義在於當錯誤發生時,強制終止當前程式。

上述提到的對 nil 強制解包導致的崩潰是斷言和先決條件中的一種。而它們還包含下面幾種情況:

  • 資料越界訪問
  • 算術溢位
  • 未捕獲的異常
  • 程式碼中的自定義斷言

2.4 作業系統“殺死”應用導致的崩潰

某些情況下,系統處於保護目的,會將一些異常的應用“殺死”。以下幾種場景可能觸發系統將應用“殺死”:

  • 看門狗事件,主執行緒長時間無響應
  • 裝置過度發燙
  • 記憶體消耗殆盡
  • 非法的應用簽名

以上幾種場景導致的崩潰,其崩潰日誌可以在上面提到的 Device Window 中檢視,Organizer Window 並不一定能夠收集到這些日誌。更多細節可以參考蘋果的這個技術講座 Understanding and Analyzing Application Crash Reports

先來看一個關於看門狗的例子。

WWDC 2018:理解崩潰以及崩潰日誌

上面的崩潰型別為 EXC_CRASH (SIGKILL)SIGKILL 一般代表的是系統終止了程式的執行,這種訊號無法被應用捕獲,進而也就無法處理。終止原因為 Namespace SPRINGBOARD, Code 0x8badf00d,如果你有檢視上面提到的關於崩潰日誌的講座,你應該會知道 Code 0x8badf00d 代表什麼。從終止描述中來看,是由於啟動時長超過了 19.97 秒。

這次總算知道為什麼看門狗對應的 code0x8badf00d 了,從這次蘋果工程師的發音上來看,這個 code 的發音同 ate bad food

2.4.1 如何避免啟動超時

應用稽核被拒的比較常見的原因就包含啟動超時這一項。那麼如何來避免這種情況發生呢?蘋果工程師給了我們這些建議:

  • 在真機上測試,因為看門狗在模擬器以及除錯階段是被禁用的
  • 在低效能裝置上測試,高效能裝置響應肯定會快,無法體現出真實效果

2.4.2 如何避免記憶體問題

常見的記憶體錯誤包含:過度釋放、野指標(訪問已釋放物件)、記憶體訪問越界(比如 C 陣列)。我們還是通過一個日誌來分析一下具體問題。

WWDC 2018:理解崩潰以及崩潰日誌

由上圖中標註的1,我們知道崩潰型別為 EXC_BAD_ACCESS(SIGSEGV),這種型別崩潰主要是有兩種情況導致:

  • 對只讀的記憶體地址進行寫操作
  • 訪問不存在的記憶體地址

通過崩潰棧中的objc_releaseobject_dispose 等,我們更加確定這是由於記憶體問題導致的崩潰。我們通過這幾個線索可以知道,LoginViewController 例項在呼叫 deinit 方法銷燬相關屬性的時候,發生了記憶體問題,進而導致崩潰的產生。

我們回到日誌的第一部分中的Exception Codes,蘋果的工程師說可以根據經驗以及日誌中的相關資訊得出結論,對應的 BAD_ADDRESS0x7fdd5e70700。原因是 0x7fdd5e70700 剛好在日誌中的這一段 MALLOC_TINY 00007fdd5e400000-00007fdd5e800000 地址範圍內。

一些關於記憶體及釋放的基礎

WWDC 2018:理解崩潰以及崩潰日誌

Objective-C 物件以及一些 Swift 物件的記憶體佈局如圖,當一個物件有效(未釋放)時以 isa 開始,isa 指向它所屬的類。objc_release 主要是讀取物件的 isa 指標,然後將 isa 指標解除對 Class 的引用。

正常情況下,一切都能照常工作。如果物件已經被釋放,會發生什麼呢?free 函式呼叫後,會將物件刪除,並且將其插入到包含了其它已釋放物件組成的連結串列中,同時將之前 isa 區域指向連結串列中下一個已釋放物件。

WWDC 2018:理解崩潰以及崩潰日誌
WWDC 2018:理解崩潰以及崩潰日誌

當之前的 isa 記憶體區域被寫入成 rotated free list 指標時,意味著訪問這個地址返回的將是一個無效的記憶體地址,進而導致崩潰。所以當 objc_release 去解除 isa 引用時,訪問到的是 rotated free list,所以崩潰就發生了。

所以可以分析出,肯定是在釋放某個屬性時,該屬性已經被釋放。我們能知道具體是哪個屬性導致的麼?答案是肯定的。

目前從崩潰的那一行來看,__ivar_destroyer 是編譯器幫我們自動生成的函式,所以我們無從知曉具體是哪一行導致的問題。我們只知道這個類有如圖三個屬性:

WWDC 2018:理解崩潰以及崩潰日誌

但是從 @objc LoginViewController.__ivar_destroyer + 42 可以獲取到一些資訊,+42 代表著彙編裡面的該函式的偏移量。我們可以對 __ivar_destroyer 函式進行反彙編,然後看偏移量為42對應獲取的是哪個屬性,在 Xcode 中可以使用 lldb 除錯。

WWDC 2018:理解崩潰以及崩潰日誌

斷點後分別輸入上圖中黃色字的命令,分別為 command script import lldb.macosx.crashlogcrashlog /Users/.../RideSharingApp-2018-05-24-1.crash,後面的路徑需要替換成你的崩潰日誌路徑。Xcode 會自動檢索二進位制檔案以及對應的 dSYM 檔案,然後符號化顯示在 lldb 控制檯中。然後我們找到崩潰處的地址,執行如下命令,即可得到對應的反彙編程式碼:

WWDC 2018:理解崩潰以及崩潰日誌

我們不需要理解每一行彙編的意思,每行後面的註釋可以幫助我們理解,根據註釋可以知道 1、2、3 處程式碼分別代表著 userNamedatabaseviews 的釋放。回到上面提到的 +42,我們找到第3處的第一行,有一點需要注意的是大部分情況下彙編的偏移地址是返回地址,所以呼叫 objc_release 是在上一行。所以可以判斷出是在釋放 database 時出現了問題。雖然我們目前還不知道具體問題所在,但是可以通過這些資訊縮小查詢問題的範圍,可以查詢使用到 database 的地方,來找到真正的問題所在。

2.4.2 日誌分析總結

  • 理解崩潰日誌產生的原因
  • 檢查崩潰棧資訊
  • 使用反彙編幫我們找到更多線索來分析 bad address 問題

2.4.3 常見記憶體錯誤

  • objc_msgSend 或者 retain/release 崩潰

    WWDC 2018:理解崩潰以及崩潰日誌

  • 無法識別的方法異常

    WWDC 2018:理解崩潰以及崩潰日誌

  • abort() inside malloc/free

2.5 日誌分析建議

  • 不要只關注崩潰發生的那一行程式碼,多檢視一下和崩潰相關的程式碼,比如上面那個崩潰程式碼並不是真正導致 bug 出現的原因
  • 檢視所有呼叫棧,不要只關注崩潰所線上程的呼叫棧,非崩潰執行緒呼叫棧可以幫助我們檢視崩潰時應用所處狀態
  • 多檢視一些崩潰日誌,有些時候很多崩潰日誌都是崩潰在同一個地方,但是某些崩潰日誌會包含更多的資訊
  • 使用 Xcode 提供的工具來複現記憶體問題,比如 Address Sanitizer 或者 Zombies

3. 多執行緒問題

3.1 崩潰日誌中多執行緒問題的一些“症狀”

  • 最難復現和診斷的一類 bug
  • 多執行緒問題通常會引起記憶體競爭
  • 多個執行緒執行著相似程式碼
  • 同一個 bug 可能會有不同的崩潰日誌

3.2 使用 Thread Sanitizer 檢測多執行緒問題

多執行緒問題即使我們拿到日誌大概率情況下也無法分析問題所在,即使連著 Xcode 除錯也不一定能夠穩定復現,即使運氣好能復現也可能分析不出具體問題。所以我們可以藉助 Xcode 提供的工具來幫我們分析,這個工具就是 Thread Sanitizer。通過快捷鍵 Cmd+shift+,,然後選則 Diagnostics tab,勾選 Thread Sanitizer 即可。如下圖所示

WWDC 2018:理解崩潰以及崩潰日誌

  • 可穩定復現多執行緒 bug
  • 在模擬器下也可進行
  • 只查詢當前正在執行的程式碼的問題

3.3 實用建議

在建立 GCD Queue(NS)OperationQueue(NS)Thread 時,使用自定義名稱,方便後續除錯以及崩潰日誌內檢視。


let queue = DispatchQueue(label: "com.example.myapp.networking")

let operationQueue = OperationQueue()
operationQueue.name = "Networking OperationQueue"
 
let thread = Thread(...)
thread.name = "Networking Thread"
複製程式碼

3.4 額外建議

  • 使用真機測試
  • 嘗試復現,從使用者處拿到崩潰日誌後根據呼叫棧嘗試去復現問題
  • 使用工具來查詢難以復現的 bug,下面兩個工具的更多使用方式可以參考 WWDC 2016 Session 412 Thread Sanitizer and Static Analysis
    • 使用 Address Sanitizer 來檢視記憶體問題
    • 使用 Thread Sanitizer 來檢視多執行緒問題

相關文章