瞭解和分析iOS Crash

做者:孟嵩,騰訊高級開發工程師 html

商業轉載請聯繫騰訊WeTest得到受權,非商業轉載請註明出處。
node

原文連接:wetest.qq.com/lab/view/40…程序員




WeTest 導讀

北京時間凌晨一點,蘋果一年一度的發佈會如期而至。新機型的發佈又會讓適配相關的同窗忙上一陣子啦,而且iOS Crash的問題始終伴隨着移動開發者。本文將從三個階段,由淺入深的介紹如何看懂並分析一篇crash報告,一塊兒身臨其境去讀懂它吧。sql


翻譯自蘋果官方文檔:Understanding and Analyzing Application Crash Reports
數據庫


孟嵩:這篇萬字長文,大概先後翻譯了一個月,「寫」了三遍:第一遍是直譯,第二遍是把直譯改爲程序員看着舒服的「行話」,第三遍是把原文裏說的過於抽象或者簡單的部分加上個人註解(你們看見全部以孟嵩開頭的部分)。數組


當app發生crash時會產生crash report,這對咱們定位crash的緣由很是有幫助。該篇重點介紹瞭如何符號化、看懂並解析一篇crash Report。xcode


孟嵩:安全

開篇給出了這個文檔的三個階段,由淺入深爲:bash

1. 符號化,把不可讀的文檔轉成可讀網絡

2. 看懂,意思就是知道文檔裏哪一個部分表達的什麼

3. 解析,意思就是能從文檔中定位問題,獲取解決問題的有價值的信息。


ps:文內展現代碼都可左右滑動查看


介紹

當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比較相似。



[ 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信息,即使構建自於同一個源代碼。


孟嵩:意思就是說,同一次構建,app+dSYM+UUID是一套的。若是這幾個文件不屬於同一次構建,即使是相同的源代碼,互相之間在符號化這個事情上也沒法互相工做。


2. 當你爲了分發app而選擇Archive(存檔)時,Xcode會把app的二進制信息和.dYSM文件存儲在你的home文件夾下的某個地方。你能夠在Xcode的Organizer裏面經過」Archived」選項找到全部你存檔過的app。 更多存檔app的細節,請點擊官方文檔-分發你的App一文。


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


3. 若是你是經過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裏作符號化。


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


5. 用戶能夠經過調試已部署的iOS APP裏提到的方法來直接從他們的設備裏得到crash report。若是你經過AdHoc或者企業證書分發app,這是你惟一能從用戶獲取crash report的方法。


6. 從設備上直接獲取的crash report是沒有被符號化的,你須要經過Xcode來符號化。Xcode會結合dSYM文件和你app的二進制信息把堆棧裏的每個地址對應到源代碼中。處理後的結果就是一個符號化過的crash report。


7. 若是用戶願意和Apple共享診斷信息,或者用戶經過TestFlight下載了你的beta版本app,那crash report會被上傳到App Store。


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


9. 這些符號化後的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。


孟嵩:這裏原文很拗口,大概意思就是須要的東西都在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信息和sSYM信息替換一下)


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用處有限。



[ 相同堆棧信息下的不一樣程度的符號化 ]


用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信息名列表中找到那個名字,記下來架構名和加載的地址。


孟嵩:例如在下圖裏,咱們想符號化的部分就是0x00000001000effdc,binary信息名是The Elements,底部能找到對應的名字的架構名稱是arm64,加載地址是0x1000e4000。



[ 在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上。



[ 定位一個二進制鏡像 ]


1. 在Xcode沒法符號化的堆棧裏找一行,注意第二列的binary信息的名字。

2. 在crash report的底部中的二進制信息列表裏找到那個名字。這個列表包含了每個crash事故現場存在於進程裏的二進制信息的UUID。


孟嵩:本例中須要關注的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,8Process: TheElements [303]Path: /private/var/containers/Bundle/Application/888C1FA2-3666-4AE2-9E8E-62E2F787DEC1/TheElements.app/TheElementsIdentifier: com.example.apple-samplecode.TheElementsVersion: 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。


孟嵩: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掉了,好比看門狗機制。


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


[ 看門狗觸發時機 ]


· 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)而不是強制解包來得到其值。


孟嵩:可選綁定,就是相似以下語句的使用

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 Information 的Termination 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 。

這次蘋果新發布的6.1英寸iPhone XR、5.8英寸iPhone XS、6.5英寸iPhone XS Max,WeTest將會第一時間收入機房,關注WeTest官方報道,獲取最新機型上線時間。


點擊:wetest.qq.com/product/clo… 更多「雲真機」產品等你來用


若是使用當中有任何疑問,歡迎聯繫騰訊WeTest企業QQ:2852350015。

相關文章
相關標籤/搜索