如何處理iOS崩潰crash大解析

前言

iOS崩潰是讓iOS開發人員比較頭痛的事情,app崩潰了,說明代碼寫的有問題,這時如何快速定位到崩潰的地方很重要。調試階段是比較容易找到出問題的地方的,但是已經上線的app並分析崩潰報告就比較麻煩了。

之前我總是找到一個改一個,並靠別人測試重現來找出問題的地方,這樣往往比較耗費時間。而且比較難找到原因的時候每次都是到網上找各種資源搜索,解決了之後也沒有認真分析原因及收集,時間長了之後就會忘記原來解決過的問題,別人來問我的時候我也不能很快找到答案。所以這裏寫一個關於崩潰的文章,以便之後自己查詢用。

這裏也會開始寫一個demo,爭取把所有的崩潰原因都寫進demo裏。

1.崩潰分析

1.1.崩潰日誌(crash log)

1.1.1.xcode中查看崩潰信息

xcode->Window->Organizer->Crashes

本人5年iOS開發經驗,曾就職於阿里巴巴。 善於把艱澀的iOS知識轉化爲通俗易懂的白話文字,同時也歡迎大家加入小編的iOS交流羣 413038000 ,羣裏會提供相關面試資料,書籍歡迎大家入駐!

1.1.2.根據符號表來監測奔潰位置

什麼是符號表

符號表就是指在Xcode項目編譯後,在編譯生成的二進制文件.app的同級目錄下生成的同名的.dSYM文件。

.dSYM文件其實是一個目錄,在子目錄中包含了一個16進制的保存函數地址映射信息的中轉文件,所有Debug的symbols都在這個文件中(包括文件名、函數名、行號等),所以也稱之爲調試符號信息文件。

符號表有什麼用

符號表就是用來符號化 crash log(崩潰日誌)。crash log中有一些方法16進制的內存地址等,通過符號表就能找到對應的能夠直觀看到的方法名之類。

如何得到.dsYM文件

我們在Archive的時候會生成.xcarchive文件,然後顯示包內容就能夠在裏面找到.dsYM文件和.app文件。

如何使用.dsYM

1.友盟.dsYM分析

如果是使用友盟的話,我們能在錯誤列表裏看到一些錯誤,然後可以導出奔潰信息,導出的文件爲.csv文件。友盟有一個分析工具,使用那個工具可以看到一些錯誤的函數,行號等。但是很容易分析失敗,不知道爲什麼?

注意:使用的時候要確保你的.xcarchive在 ~/Library/Developer/Xcode/或該路徑的子目錄下。

.xcarchive裏的.dsYM文件和.app文件是有對應的UUID的。然後你的錯誤詳情裏也是有UUID,只有當UUID相等時才能分析對。

我犯的錯誤:因爲我們是兩個人開發,Archive的時候都是在另一個人的電腦上Archive的,所以我的電腦里根本沒有對應的.xcarchive文件。所以我在我電腦上用友盟的分析工具分析是時候是監測不出來錯誤的。

2.第三方小工具.dsYM分析

或者自己找到.xcarchive文件和錯誤內存地址(友盟錯誤詳情裏標綠色的爲錯誤內存地址)。然後通過一個小應用來分析出對應的函數。應用下載地址,具體可參考文章dSYM 文件分析工具

注意拿來分析的xcarchive名字不要有空格或特殊字符,直接用最簡單的數字就好了

下圖是我友盟裏的錯誤信息,可以分析的內存地址就是標綠的地方,圖中zhefengle就是你的app名,這部分後面的地址就是可以解析符號化的地址:

下圖是分析工具分析上面的錯誤內存地址:

3.命令行工具symbolicatecrash

symbolicatecrash是xcode的一個符號化crash log的命令行工具。使用方法也就是導出.crash文件(crash log)和找到.dsYM文件,然後進行分析。

如何使用查看[iOS]使用symbolicatecrash分析crash文件

4.還有命令行工具atos

如果你有多個“.ipa”文件,多個".dSYMB"文件,你並不太確定到底“dSYM”文件對應哪個".ipa"文件,那麼,這個方法就非常適合你。

特別當你的應用發佈到多個渠道的時候,你需要對不同渠道的crash文件,寫一個自動化的分析腳本的時候,這個方法就極其有用。

簡單使用方法命令行工具atos

1.1.3.奔潰日誌分析

參考iOS應用崩潰日誌分析裏面有很詳細的分析介紹。

以上是一個完整的崩潰日誌。其實友盟錯誤詳情裏的就是上圖的第4部分。

如何得到崩潰日誌

1.把設備連上電腦,得到自己設備的崩潰日誌

崩潰日誌可以從xcode裏打開Devices看到對應手機的一些奔潰信息。點擊下圖的View Device Logs就能看到崩潰日誌。

2.使用第三方崩潰管理工具

我暫時只使用過友盟,友盟裏面有錯誤分析,就是截取的崩潰日誌。

3.自己截取崩潰日誌

自己寫入代碼,然後截取到崩潰日誌,把崩潰日誌發送到開發者郵箱裏。
iOS Crash(崩潰)調試技巧這篇文章中有介紹如何截取崩潰日誌併發送到郵箱。

分析崩潰日誌

崩潰日誌中的(3)異常

Exception Type異常類型
通常包含1.7中的Signal信號和EXC_BAD_ACCESS。

Exception Codes:異常編碼
0x8badf00d: 讀做 “ate bad food”! (把數字換成字母,是不是很像 :p)該編碼表示應用是因爲發生watchdog超時而被iOS終止的。 通常是應用花費太多時間而無法啓動、終止或響應用系統事件。

0xbad22222: 該編碼表示 VoIP 應用因爲過於頻繁重啓而被終止。

0xdead10cc: 讀做 “dead lock”!該代碼表明應用因爲在後臺運行時佔用系統資源,如通訊錄數據庫不釋放而被終止 。

0xdeadfa11: 讀做 “dead fall”! 該代碼表示應用是被用戶強制退出的。根據蘋果文檔, 強制退出發生在用戶長按開關按鈕直到出現 “滑動來關機”, 然後長按 Home按鈕。強制退出將產生 包含0xdeadfa11 異常編碼的崩潰日誌, 因爲大多數是強制退出是因爲應用阻塞了界面。

崩潰日誌中的(4)線程回溯

這部分提供應用中所有線程的回溯日誌。 回溯是閃退發生時所有活動幀清單。它包含閃退發生時調用函數的清單。看下面這行日誌:

它包括四列:
幀編號—— 此處是2。(數子從大到小爲發生的順序)
二進制庫的名稱 ——此處是 XYZLib.
調用方法的地址 ——此處是 0x34648e88.
第四列分爲兩個子列,一個基本地址和一個偏移量。此處是0×83000 + 8740, 第一個數字指向文件,第二個數字指向文件中的代碼行。

1.2.野指針分析方法(Enable Malloc Scribble)

介紹

因爲野指針的原因發生崩潰是常常出現的事,而且比較隨機。關於一些原因及概念後面我們會講到。所以我們要提高野指針的崩潰率好來幫我們快速找到有問題的代碼。

對象釋放後只有出現被隨機填入的數據是不可訪問的時候纔會必現Crash。

這個地方我們可以做一下手腳,把這一隨機的過程變成不隨機的過程。對象釋放後在內存上填上不可訪問的數據,其實這種技術其實一直都有,xcode的Enable Scribble就是這個作用。

更加詳細的介紹可以參考:如何定位Obj-C野指針隨機Crash

DSCrashDemo這個demo裏有上面這篇文章裏寫的關於提高野指針崩潰率的例子。

1.3.殭屍模式(NSZombieEnabled)

介紹

啓用了NSZombieEnabled的話,它會用一個殭屍來替換默認的dealloc實現,也就是在引用計數降到0時,該殭屍實現會將該對象轉換成殭屍對象。殭屍對象的作用是在你向它發送消息時,它會顯示一段日誌並自動跳入調試器。

所以當啓用NSZombieEnabled時,一個錯誤的內存訪問就會變成一條無法識別的消息發送給殭屍對象。殭屍對象會顯示接受到得信息,然後跳入調試器,這樣你就可以查看到底是哪裏出了問題。

所以這時一般崩潰的原因是:調用了已經釋放的內存空間,或者說重複釋放了某個地址空間。

如何找出問題

1.NSZombieEnabled

打開NSZombieEnabled之後,如果遇到對應的崩潰類型既調用了已經釋放的內存空間,或者說重複釋放了某個地址空間。那麼就能在GDB中看到對應的輸出信息。

比如會出現如下這樣的問題:
<code>[__NSArrayM addObject:]: message sent to deallocated instance 0x7179910</code>

2.MallocStackLoggingNoCompact

如果崩潰是發生在當前調用棧,通過上面的做法,系統就會把崩潰原因定位到具體代碼中。但是,如果崩潰不在當前調用棧,系統就僅僅只能把崩潰地址告訴我們,而沒辦法定位到具體代碼,這樣我們也沒法去修改錯誤。這時就可以修改scheme,讓xcode記錄每個地址alloc的歷史,這樣我們就可以用命令把這個地址還原出來。
如圖:(跟設置<code>NSZombieEnabled</code>一樣,添加<code>MallocStackLoggingNoCompact</code>,並且設置爲YES)

這樣,當出現崩潰原因是message sent to deallocated instance 0x7179910,我們可以使用以下命令,把內存地址還原:

<code>(gdb) nfo malloc-history 0x7179910</code>

也可以使用下面的命令
<code>(gdb) shell malloc_history {pid/partial-process-name} {address}
</code>

這篇文章中有介紹MallocStackLoggingNoCompact的使用。

總結

還有官方文檔Enabling the Malloc Debugging Features介紹了類似<code>NSZombieEnabled</code>和<code>MallocStackLoggingNoCompact</code>這類的環境變量的作用。

TODO:翻譯Enabling the Malloc Debugging Features這篇文章,寫對應的demo測試這類變量設置後如何找出內存出錯問題。

1.4.Enable Address Sanitizer(地址消毒劑)

設置這個參數後就能看到一些更詳細的錯誤信息提示,甚至會有內存使用情況的展示。

C語言是一門危險的語言,內存安全是一個主要的問題。C語言中根本沒有內存安全可言。像下面的代碼,會被正常的編譯,而且可能正常運行:
<code>char *ptr = malloc(5);
ptr[12] = 0;</code>
對於內存安全的驗證已經有一些解決方案了。如Clang的靜態代碼分析,可以從代碼中查找特定類型的內存安全問題。如Valgrind之類的程序可以在運行時檢測到不安全的內存訪問。

Address Sanitizer是另外一種解決方案。它使用了一種新的方法,有利有弊。但仍不失爲一個查找代碼問題的有力工具。

這類工具的理論依據是:訪問內存時,通過比較訪問的內存和程序實際分配的內存,驗證內存訪問的有效性,從而在bug發生時就檢測到它們,而不會等到副作用產生時纔有所察覺。

malloc函數總是最少分配16個字節。爲了儲存針對標準malloc的內存的保護,需要分配內存到16字節的範圍內,因此,若分配的內存大小不是16字節的整數倍,餘出的幾個字節將不受保護。

Address Sanitizer會追蹤受限內存,使用了一種簡單但是很巧妙的方法:它在進程的內存空間上保存了一個固定的區域,叫做“影子內存區”。用內存消毒劑的術語來說,一個被標記爲受限的內存被稱作“中毒”內存。“影子內存區”會記錄哪些內存字節是中毒的。通過一個簡單的公式,可以將進程中的內存空間映射到“影子內存區”中,即:每8字節的正常內存塊映射到一個字節的影子內存上。在影子內存上,會跟蹤這8字節的“中毒狀態”。

Address Sanitizer這篇文章詳細介紹了Enable Address Sanitizer,對應的中文翻譯在Xcode 7上直接使用Clang Address Sanitizer

1.5.Static Analyzer(靜態分析)

Static Analyzer是一個非常好的工具去發現編譯器警告不會提示的問題和一些個人的內錯泄露和死存儲(不會用到的賦了值的變量)錯誤。這個方法可能大大的提高內存使用和性能,以及提升應用的整體穩定性和代碼質量。

打開方式:Xcode->Product-Analyze
然後我們就能看到如下藍色箭頭所示的一些有問題的代碼。

1.6.unrecognized selector send to instancd 快速定位

在debug navigator的斷點欄裏添加Create Symbolic Breakpoint。

在Symbolic中填寫如下方法簽名:
<code>-[NSObject(NSObject) doesNotRecognizeSelector:] </code>

設置完成後再遇到類似的錯誤就會定位到具體的代碼。

1.7.Signal和EXC_BAD_ACCESS錯誤分析

1.7.1.Signal

什麼是Signal

在計算機科學中,信號(英語:Signals)是Unix、類Unix以及其他POSIX兼容的操作系統中進程間通訊的一種有限制的方式。它是一種異步的通知機制,用來提醒進程一個事件已經發生。當一個信號發送給一個進程,操作系統中斷了進程正常的控制流程,此時,任何非原子操作都將被中斷。如果進程定義了信號的處理函數,那麼它將被執行,否則就執行默認的處理函數。

在iOS中就是未被捕獲的Objective-C異常(NSException),導致程序向自身發送了SIGABRT信號而崩潰。

Signal信號的類型

SIGABRT–程序中止命令中止信號
SIGALRM–程序超時信號
SIGFPE–程序浮點異常信號
SIGILL–程序非法指令信號
SIGHUP–程序終端中止信號
SIGINT–程序鍵盤中斷信號
SIGKILL–程序結束接收中止信號
SIGTERM–程序kill中止信號
SIGSTOP–程序鍵盤中止信號
SIGSEGV–程序無效內存中止信號
SIGBUS–程序內存字節未對齊中止信號
SIGPIPE–程序Socket發送失敗中止信號
iOS異常捕獲這篇文章中有對各種信號的解釋。

SIGABRT

就crash而言,SIGABRT是一個比較好解決的,因爲他是一個可掌控的crash。App會在一個目的地終止,因爲系統意識到app做了一些他不能支持的事情。

通常, SIGABRT 異常是由於某個對象接收到未實現的消息引起的。 或者,用簡單的話說,在某個對象上調用了不存在的方法。

SIGSEGV

SIGSEGV程序無效內存中止信號,一般是表示內存不合法,

SIGBUS

SIGBUS程序內存字節未對齊中止信號,

截取Signal和Exception從容的崩潰

DSSignalHandlerDemo
這是一個防止奔潰的源碼,可以使一些原本會奔潰的操作彈出UIAlertView。裏面寫了兩種信號量的崩潰:SIGSEGV、SIGABRT,還有一些信號大家可以寫上去提個PR給我。

下圖爲源碼的運行圖,其中Signal中的Signal(EGV)第一次點擊的時候能彈出alert,如果第二次點擊就沒有崩潰和alert,感覺像卡死一樣。

而Signal(BRT)中的這種信號錯誤多次點擊也是沒有問題的還是能繼續下去。個人猜測可能是SIGSEGV這種信號錯誤會導致了整個進程掛了。

注意:測試的時候如果測試Signal類型的崩潰,不要在xcode下的debug模式進行測試。因爲系統的debug會優先去攔截。應該build好應用之後直接點擊運行app進行測試。

1.7.2.EXC_BAD_ACCESS

EXC_BAD_ACCESS是一個比較難處理的crash了,當一個app進入一種毀壞的狀態,通常是由於內存管理問題而引起的時,就會出現出現這樣的crash。通常1.7.1中的Signal信號錯誤都會提醒EXC_BAD_ACCESS。

文中1.3就介紹了用一些變量設置來找出這類錯誤。


2.崩潰類型收集

2.1.新老操作系統兼容

原因

開發人員在進行開發的時候,常常使用的是某個操作系統版本,所以在開發人員進行開發測試的那個系統版本上基本不會出現問題。但在其他版本上開發人員無法進行完全的測試,這就導致了在新系統上運行正常,但在舊系統上卻崩潰的情況。

在新 iOS 上正常的應用,到了老版本 iOS 上秒退最常見原因是系統動態鏈接庫或Framework無法找到。這種情況通常是由於 App 引用了一個新版操作系統裏的動態庫(或者某動態庫的新版本)或只有新 iOS 支持的 Framework,而又沒有對老系統進行測試,於是當 App 運行在老系統上時便由於找不到而秒退。

還有就是有些方法是新版操作系統才支持的,而又沒有對該方法是否存在於老系統中做出判斷。這種情況其實還是比較難出現的,除非開發人員太low了,因爲這類方法在xcode編碼時編輯器都會有提醒的。

解決

這種問題一般就是用戶升級操作系統或者開發人員修改問題以兼容老系統。

2.2.本地存儲的數據結構改變

原因

程序在升級時,修改了本地存儲的數據結構,但是對用戶既存的舊數據沒有做好升級,結果導致初始化時因爲無法正確讀取用戶數據而秒退。

解決

第一種:是把服務端傳過來的一些信息保存在本地,使用的時候從本地數據庫取。

剛開始的時候我是第一次從服務端得到數據的時候直接解析成對應的model然後存入plist文件裏面。這時就有一個問題,比如服務端新傳了字段<code>newId</code>,但是我舊版model裏面沒有定義過,存入本地的數據還是沒有這個字段。

然後等我升級了程序,新程序裏model,定義了這個<code>newId</code>字段,但是舊版裏面數據已經保存過一遍了沒有這個字段。這時再去取就取不到了。

所以後來我就把存儲時解析數據改成了讀取時解析數據。就是不管服務端傳什麼數據都把它存下來,然後在使用的時候再把它解析成對應的model,這樣就不會丟失字段了。

第二種:自己的一些數據存儲在本地SQLlite,新版的時候表結構改了。

SQLlite只支持更改一個表的名字,或者向表中增加一個字段(列),但是我們不能刪除一個已經存在的字段,或者更改一個已經存在的字段的名稱、數據類型、限定符等等。

這種就是有時候新版又添加字段了,或者改變了字段的名稱了。一般來說原有的字段名稱不應該改變,但是添加新字段是常有的事。

一般做法是在第一次創建表的時候加一些冗餘字段,以防後面不時之需。但是如果真沒辦法需要在舊錶上增加新字段了,那就要做數據遷移了。

這裏有一個庫在FMDB基礎上管理SQLlite數據庫了,可以用來做數據遷移用。FMDBMigrationManager

TODO:做一個數據遷移的demo

2.3.訪問的數據爲空或訪問數據類型不對

原因

這類情況是比較常見的,後端傳回了空數據,客戶端沒有做對應的判斷繼續執行下去了,這樣就產生了crash。或者自己本地的某個數據爲空數據而去使用了。還有就是訪問的數據類型不是期望的數據類型而產生崩潰。

解決

第一種:

服務端都加入默認值,不返回空內容或無key,但是服務端往往會不太願意改,還有就是有些確實應該無值的話key也不用傳,減少數據量的傳輸。

第二種:

這種就是客戶端自己做判斷,如果每次都是自己去if判斷是否爲空或格式是否正確那肯定是比較麻煩的。所以這裏用到了NSArray和NSDictionary的Category。一般我們訪問的數據都是NSArray或NSDictionary,所以在取值方法裏面做一下判斷,返回正確的數據類型或默認值即可。

DSCategories這裏面有兩個Category<code>NSArray+SafeAccessM</code>和<code>NSDictionary+SafeAccess</code>可以看一下。

2.4.操作了不該操作的對象,野指針之類

2.4.1野指針介紹

iOS中有空指針和野指針兩種概念。

空指針是沒有存儲任何內存地址的指針。如<code>Student *s1 = NULL;</code>和<code>Student *s2 = nil;</code>

而野指針是指指向一個已刪除的對象("垃圾"內存既不可用內存)或未申請訪問受限內存區域的指針。野指針是比較危險的。因爲野指針指向的對象已經被釋放了,不能用了,你再給被釋放的對象發送消息就是違法的,所以會崩潰。

空指針和野指針這篇文章介紹了空指針和野指針的概念。

2.4.2野指針崩潰情況

野指針訪問已經釋放的對象crash其實不是必現的,因爲dealloc執行後只是告訴系統,這片內存我不用了,而系統並沒有就讓這片內存不能訪問。

所以野指針的奔潰是比較隨機的,你在測試的時候可能沒發生crash,但是用戶在使用的時候就可能發生crash了。

注意:arc環境比非arc環境更少出現野指針。

現實出現問題大概是下面幾種可能的情況:

  1. 對象釋放後內存沒被改動過,原來的內存保存完好,可能不Crash或者出現邏輯錯誤(隨機Crash)。
  2. 對象釋放後內存沒被改動過,但是它自己析構的時候已經刪掉某些必要的東西,可能不Crash、Crash在訪問依賴的對象比如類成員上、出現邏輯錯誤(隨機Crash)。
  3. 對象釋放後內存被改動過,寫上了不可訪問的數據,直接就出錯了很可能Crash在objc_msgSend上面(必現Crash,常見)。
  4. 對象釋放後內存被改動過,寫上了可以訪問的數據,可能不Crash、出現邏輯錯誤、間接訪問到不可訪問的數據(隨機Crash)。
  5. 對象釋放後內存被改動過,寫上了可以訪問的數據,但是再次訪問的時候執行的代碼把別的數據寫壞了,遇到這種Crash只能哭了(隨機Crash,難度大,概率低)!!
  6. 對象釋放後再次release(幾乎是必現Crash,但也有例外,很常見)。

參考下面這張圖:

2.5.內存處理不當

說到因爲內存處理不當崩潰就要涉及到內存管理問題了。內存管理是軟件開發中一個重要的課題。iOS自從引入ARC機制後,對於內存的管理開發者好像輕鬆了很多,但是還會發生一些內存泄露之類的問題。

對於這一塊知識點需要了解ARC的一些機制,還有用instruments排查內存泄露問題等。這部分我後面會專門寫一篇文章進行闡述。

2.6.主線程UI長時間卡死,被系統殺掉

主線程被卡住是非常常見的場景,具體表現就是程序不響應任何的UI交互。這時按下調試的暫停按鈕,查看堆棧,就可以看到是到底是死鎖、死循環等,導致UI線程被卡住。

這部分需要研究多線程,還有如何看調試欄裏的線程的信息。

iOS調試裏的進程跟線程我這篇文章中有介紹多線程及死鎖的原因。

DSCrashDemo中有寫關於死鎖的例子。

2.7.多線程之間切換訪問引起的crash

多線程引起的崩潰大部分是因爲使用數據庫的時候多線程同時讀寫數據庫而造成了crash。
多線程導致的iOS閃退分析這篇文章就是關於多線程crash的調試。


3.參考文章及源碼

文章
iOS SQLite數據庫遷移
如何定位Obj-C野指針隨機Crash(一):先提高野指針Crash率
iOS如何查看崩潰信息
分析iOS Crash文件:符號化iOS Crash文件的3種方法
使用SIGNAL讓APP能夠在從容崩潰
Segmentation fault對於Signal崩潰的解釋
Exception Types in iOS crash logs stackoverflow上關於Signal的問題

源碼
如何從容的崩潰源碼截獲異常崩潰彈出alert
DSCrashDemo關於一些崩潰例子的收集

作者:齊滇大聖
鏈接:https://www.jianshu.com/p/1b804426d212

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