瞭解和分析iOS Crash

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

商業轉載請聯繫騰訊WeTest獲得授權,非商業轉載請註明出處。

原文鏈接:http://wetest.qq.com/lab/view/404.html

WeTest 導讀

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


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

孟嵩:這篇萬字長文,大概前後翻譯了一個月,“寫”了三遍:第一遍是直譯,第二遍是把直譯改成程序員看着舒服的“行話”,第三遍是把原文裏說的過於抽象或者簡單的部分加上我的註解(大家看見所有以孟嵩開頭的部分)。

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

孟嵩:開篇給出了這個文檔的三個階段,由淺入深爲:符號化,把不可讀的文檔轉成可讀 看懂,意思就是知道文檔裏哪個部分表達的什麼 解析,意思就是能從文檔中定位問題,獲取解決問題的有價值的信息。

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

1.png

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是一套的。如果這幾個文件不屬於同一次構建,即便是相同的源代碼,互相之間在符號化這個事情上也無法互相工作。

  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.png
                [ 圖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用處有限。

3.png
       [ 相同堆棧信息下的不同程度的符號化 ]

用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。

4.png
    [ 在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.png
      [ 定位一個二進制鏡像 ]
  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,看門狗吃到了壞的食物,不嗨森)。 看門狗觸發條件如下:

6.png

[ 看門狗觸發時機 ]

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

孟嵩:可選綁定,就是類似如下語句的使用 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加載完畢)。

KilledSIGKILL

進程收到系統指令被幹掉。請自行查看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官方報道,獲取最新機型上線時間。

點擊:http://wetest.qq.com/product/cloudphone 更多“雲真機”產品等你來用

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章