WWDC 2018 Session 414: Understanding Crashes and Crash Logshtml
查看更多 WWDC 18 相關文章請前往 老司機x知識小集xSwiftGG WWDC 18 專題目錄ios
做者簡介:@Vong,目前就任於美拍,喜歡折騰~git
人非聖賢,孰能無過。每一個人在寫代碼的時候,或多或少都會犯錯,那麼如何調試、找出問題所在呢?讓咱們跟隨蘋果工程師一塊兒瞭解一下崩潰是如何產生以及如何解決它們的吧。github
崩潰是什麼?崩潰是當應用想要作某件事的時候,被意外終止。macos
主要是如下幾方面緣由編程
NSArray
或者 Swift.Array
越界當咱們鏈接着 Xcode
進行調試的時候,遇到崩潰,大概長這個樣子。 數組
當連着調試器的時候,咱們可以拿到崩潰現場的一些調用棧以及對應的方法,當沒有連着調試器的時候,系統會將崩潰日誌存儲到磁盤當中。多線程
一般狀況下,release
模式的應用的崩潰日誌是沒有符號化的,日誌內記錄的都是地址。咱們能夠經過 Xcode
來將崩潰日誌進行符號化,解析出對應文件名、方法名以及對應崩潰在第幾行。app
獲取崩潰日誌的方式不少,咱們先來了解一下如何經過 Xcode Organizer
來獲取從 TestFlight
或App Store
下載的應用的崩潰日誌。編程語言
先來看一下下面這張圖:
下面數字 1~6 分別表明圖中標註的 1~6
App Store
或者 TestFlight
上的應用。PS:上面6個只是簡單介紹了一下主題部分,剩餘的能夠自行探索使用。好比搜索、對單個日誌作一些筆記、以及將已修復的崩潰標記爲已解決等等。
那麼如何才能在 Organizer
中獲取對應的崩潰日誌呢?很簡單,只須要作到下面幾步
Xcode
中登陸已付費的開發者賬號。App Store
或 TestFlight
時,一併上傳符號文件。Xcode Organizer
窗口,選中 Crashes
tab(快捷鍵:Cmd+Shift+6
)。鏈接上設備,打開 Xcode
,使用快捷鍵 Cmd+Shift+2
來打開 Devices Window
,選中對應設備,而後選擇 View Device Logs
,便可查看當前設備磁盤上的全部崩潰文件,找到應用對應的日誌便可展開分析。
有些時候,獲取到的崩潰日誌並無符號化。這個時候須要本身作一些額外操做,這裏能夠參考我以前在知識小集分享過的一個小 tip——iOS快速解析崩潰日誌。
Xcode
的自動化測試(獲得的是已符號化的日誌)Mac
自帶的 Console
應用,獲取 Mac
或者模擬器的崩潰日誌Xcode Organizer
的 Crashes
tab 中呈現。Xcode
會自動進行符號化。Xcode Organizer
的 Archive
tab 爲已開啓 bitcode
的應用下載 dSYM
文件。MacOS
上可見)首先從崩潰緣由中的崩潰類型開始
如上圖的崩潰類型爲 EXC_BAD_INSTRUCTION
,它表明 CPU
嘗試在執行一段不存在或無效的代碼,而致使進行被「殺死」。
而後咱們能夠找到崩潰線程的調用棧的前幾行,結合崩潰信息(若是有的話)進一步分析。找到崩潰棧中第一處二進制名爲應用名稱所在那一行,進到對應文件對應的代碼行數進行查看(如上圖中標紅的那一行),而後進一步分析。上圖中的崩潰能夠很明顯看出其緣由是對 nil
進行了強制解包。
斷言和先決條件的意義在於當錯誤發生時,強制終止當前進程。
上述提到的對 nil
強制解包致使的崩潰是斷言和先決條件中的一種。而它們還包含下面幾種狀況:
某些狀況下,系統處於保護目的,會將一些異常的應用「殺死」。如下幾種場景可能觸發系統將應用「殺死」:
以上幾種場景緻使的崩潰,其崩潰日誌能夠在上面提到的 Device Window
中查看,Organizer Window
並不必定可以收集到這些日誌。更多細節能夠參考蘋果的這個技術講座 Understanding and Analyzing Application Crash Reports。
先來看一個關於看門狗的例子。
上面的崩潰類型爲 EXC_CRASH (SIGKILL)
,SIGKILL
通常表明的是系統終止了進程的運行,這種信號沒法被應用捕獲,進而也就沒法處理。終止緣由爲 Namespace SPRINGBOARD, Code 0x8badf00d
,若是你有查看上面提到的關於崩潰日誌的講座,你應該會知道 Code 0x8badf00d
表明什麼。從終止描述中來看,是因爲啓動時長超過了 19.97 秒。
此次總算知道爲何看門狗對應的
code
是0x8badf00d
了,從此次蘋果工程師的發音上來看,這個code
的發音同ate bad food
。
應用審覈被拒的比較常見的緣由就包含啓動超時這一項。那麼如何來避免這種狀況發生呢?蘋果工程師給了咱們這些建議:
常見的內存錯誤包含:過分釋放、野指針(訪問已釋放對象)、內存訪問越界(好比 C 數組)。咱們仍是經過一個日誌來分析一下具體問題。
由上圖中標註的1,咱們知道崩潰類型爲 EXC_BAD_ACCESS(SIGSEGV)
,這種類型崩潰主要是有兩種狀況致使:
經過崩潰棧中的objc_release
、object_dispose
等,咱們更加肯定這是因爲內存問題致使的崩潰。咱們經過這幾個線索能夠知道,LoginViewController
實例在調用 deinit
方法銷燬相關屬性的時候,發生了內存問題,進而致使崩潰的產生。
咱們回到日誌的第一部分中的Exception Codes
,蘋果的工程師說能夠根據經驗以及日誌中的相關信息得出結論,對應的 BAD_ADDRESS
爲 0x7fdd5e70700
。緣由是 0x7fdd5e70700
恰好在日誌中的這一段 MALLOC_TINY 00007fdd5e400000-00007fdd5e800000
地址範圍內。
一些關於內存及釋放的基礎
Objective-C
對象以及一些 Swift
對象的內存佈局如圖,當一個對象有效(未釋放)時以 isa
開始,isa
指向它所屬的類。objc_release
主要是讀取對象的 isa
指針,而後將 isa
指針解除對 Class
的引用。
正常狀況下,一切都能照常工做。若是對象已經被釋放,會發生什麼呢?free
函數調用後,會將對象刪除,而且將其插入到包含了其它已釋放對象組成的鏈表中,同時將以前 isa
區域指向鏈表中下一個已釋放對象。
當以前的 isa
內存區域被寫入成 rotated free list
指針時,意味着訪問這個地址返回的將是一個無效的內存地址,進而致使崩潰。因此當 objc_release
去解除 isa
引用時,訪問到的是 rotated free list
,因此崩潰就發生了。
因此能夠分析出,確定是在釋放某個屬性時,該屬性已經被釋放。咱們能知道具體是哪一個屬性致使的麼?答案是確定的。
目前從崩潰的那一行來看,__ivar_destroyer
是編譯器幫咱們自動生成的函數,因此咱們無從知曉具體是哪一行致使的問題。咱們只知道這個類有如圖三個屬性:
可是從 @objc LoginViewController.__ivar_destroyer + 42
能夠獲取到一些信息,+42
表明着彙編裏面的該函數的偏移量。咱們能夠對 __ivar_destroyer
函數進行反彙編,而後看偏移量爲42對應獲取的是哪一個屬性,在 Xcode
中可使用 lldb
調試。
斷點後分別輸入上圖中黃色字的命令,分別爲 command script import lldb.macosx.crashlog
,crashlog /Users/.../RideSharingApp-2018-05-24-1.crash
,後面的路徑須要替換成你的崩潰日誌路徑。Xcode
會自動檢索二進制文件以及對應的 dSYM
文件,而後符號化顯示在 lldb
控制檯中。而後咱們找到崩潰處的地址,執行以下命令,便可獲得對應的反彙編代碼:
咱們不須要理解每一行彙編的意思,每行後面的註釋能夠幫助咱們理解,根據註釋能夠知道 一、二、3 處代碼分別表明着 userName
、database
、views
的釋放。回到上面提到的 +42
,咱們找到第3處的第一行,有一點須要注意的是大部分狀況下彙編的偏移地址是返回地址,因此調用 objc_release
是在上一行。因此能夠判斷出是在釋放 database
時出現了問題。雖然咱們目前還不知道具體問題所在,可是能夠經過這些信息縮小查找問題的範圍,能夠查找使用到 database
的地方,來找到真正的問題所在。
bad address
問題objc_msgSend 或者 retain/release 崩潰
沒法識別的方法異常
abort() inside malloc/free
bug
出現的緣由Xcode
提供的工具來複現內存問題,好比 Address Sanitizer
或者 Zombies
多線程問題即便咱們拿到日誌大機率狀況下也沒法分析問題所在,即便連着 Xcode
調試也不必定可以穩定復現,即便運氣好能復現也可能分析不出具體問題。因此咱們能夠藉助 Xcode
提供的工具來幫咱們分析,這個工具就是 Thread Sanitizer
。經過快捷鍵 Cmd+shift+,
,而後選則 Diagnostics
tab,勾選 Thread Sanitizer
便可。以下圖所示
在建立 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"
複製代碼
Address Sanitizer
來查看內存問題Thread Sanitizer
來查看多線程問題