前言
iOS崩潰是讓iOS開發人員比較頭痛的事情,app崩潰了,說明代碼寫的有問題,這時如何快速定位到崩潰的地方很重要。調試階段是比較容易找到出問題的地方的,但是已經上線的app並分析崩潰報告就比較麻煩了。
這裏也會開始寫一個demo,爭取把所有的崩潰原因都寫進demo裏。
1.崩潰分析
1.1.崩潰日誌(crash log)
1.1.1.xcode中查看崩潰信息
xcode->Window->Organizer->Crashes
1.1.2.根據符號表來監測奔潰位置
符號表就是指在Xcode項目編譯後,在編譯生成的二進制文件.app的同級目錄下生成的同名的.dSYM文件。
.dSYM文件其實是一個目錄,在子目錄中包含了一個16進制的保存函數地址映射信息的中轉文件,所有Debug的symbols都在這個文件中(包括文件名、函數名、行號等),所以也稱之爲調試符號信息文件。
符號表就是用來符號化 crash log(崩潰日誌)。crash log中有一些方法16進制的內存地址等,通過符號表就能找到對應的能夠直觀看到的方法名之類。
如果是使用友盟的話,我們能在錯誤列表裏看到一些錯誤,然後可以導出奔潰信息,導出的文件爲.csv文件。友盟有一個分析工具,使用那個工具可以看到一些錯誤的函數,行號等。但是很容易分析失敗,不知道爲什麼?
注意:使用的時候要確保你的.xcarchive在 ~/Library/Developer/Xcode/或該路徑的子目錄下。
.xcarchive裏的.dsYM文件和.app文件是有對應的UUID的。然後你的錯誤詳情裏也是有UUID,只有當UUID相等時才能分析對。
或者自己找到.xcarchive文件和錯誤內存地址(友盟錯誤詳情裏標綠色的爲錯誤內存地址)。然後通過一個小應用來分析出對應的函數。應用下載地址,具體可參考文章dSYM 文件分析工具
注意拿來分析的xcarchive名字不要有空格或特殊字符,直接用最簡單的數字就好了
下圖是我友盟裏的錯誤信息,可以分析的內存地址就是標綠的地方,圖中zhefengle就是你的app名,這部分後面的地址就是可以解析符號化的地址:
symbolicatecrash是xcode的一個符號化crash log的命令行工具。使用方法也就是導出.crash文件(crash log)和找到.dsYM文件,然後進行分析。
如果你有多個“.ipa”文件,多個".dSYMB"文件,你並不太確定到底“dSYM”文件對應哪個".ipa"文件,那麼,這個方法就非常適合你。
特別當你的應用發佈到多個渠道的時候,你需要對不同渠道的crash文件,寫一個自動化的分析腳本的時候,這個方法就極其有用。
1.1.3.奔潰日誌分析
以上是一個完整的崩潰日誌。其實友盟錯誤詳情裏的就是上圖的第4部分。
崩潰日誌可以從xcode裏打開Devices看到對應手機的一些奔潰信息。點擊下圖的View Device Logs就能看到崩潰日誌。
自己寫入代碼,然後截取到崩潰日誌,把崩潰日誌發送到開發者郵箱裏。
iOS Crash(崩潰)調試技巧這篇文章中有介紹如何截取崩潰日誌併發送到郵箱。
Exception Type異常類型
通常包含1.7中的Signal信號和EXC_BAD_ACCESS。0xbad22222: 該編碼表示 VoIP 應用因爲過於頻繁重啓而被終止。
0xdead10cc: 讀做 “dead lock”!該代碼表明應用因爲在後臺運行時佔用系統資源,如通訊錄數據庫不釋放而被終止 。
1.2.野指針分析方法(Enable Malloc Scribble)
因爲野指針的原因發生崩潰是常常出現的事,而且比較隨機。關於一些原因及概念後面我們會講到。所以我們要提高野指針的崩潰率好來幫我們快速找到有問題的代碼。
對象釋放後只有出現被隨機填入的數據是不可訪問的時候纔會必現Crash。
這個地方我們可以做一下手腳,把這一隨機的過程變成不隨機的過程。對象釋放後在內存上填上不可訪問的數據,其實這種技術其實一直都有,xcode的Enable Scribble就是這個作用。
更加詳細的介紹可以參考:如何定位Obj-C野指針隨機Crash。
DSCrashDemo這個demo裏有上面這篇文章裏寫的關於提高野指針崩潰率的例子。
1.3.殭屍模式(NSZombieEnabled)
所以當啓用NSZombieEnabled時,一個錯誤的內存訪問就會變成一條無法識別的消息發送給殭屍對象。殭屍對象會顯示接受到得信息,然後跳入調試器,這樣你就可以查看到底是哪裏出了問題。
打開NSZombieEnabled之後,如果遇到對應的崩潰類型既調用了已經釋放的內存空間,或者說重複釋放了某個地址空間。那麼就能在GDB中看到對應的輸出信息。
比如會出現如下這樣的問題:
[__NSArrayM addObject:]: message sent to deallocated instance 0x7179910
這樣,當出現崩潰原因是message sent to deallocated instance 0x7179910,我們可以使用以下命令,把內存地址還原:
(gdb) nfo malloc-history 0x7179910
也可以使用下面的命令
(gdb) shell malloc_history {pid/partial-process-name} {address}
TODO:翻譯Enabling the Malloc Debugging Features這篇文章,寫對應的demo測試這類變量設置後如何找出內存出錯問題。
1.4.Enable Address Sanitizer(地址消毒劑)
設置這個參數後就能看到一些更詳細的錯誤信息提示,甚至會有內存使用情況的展示。
Address Sanitizer是另外一種解決方案。它使用了一種新的方法,有利有弊。但仍不失爲一個查找代碼問題的有力工具。
這類工具的理論依據是:訪問內存時,通過比較訪問的內存和程序實際分配的內存,驗證內存訪問的有效性,從而在bug發生時就檢測到它們,而不會等到副作用產生時纔有所察覺。
malloc函數總是最少分配16個字節。爲了儲存針對標準malloc的內存的保護,需要分配內存到16字節的範圍內,因此,若分配的內存大小不是16字節的整數倍,餘出的幾個字節將不受保護。
Address Sanitizer這篇文章詳細介紹了Enable Address Sanitizer,對應的中文翻譯在Xcode 7上直接使用Clang Address Sanitizer
1.5.Static Analyzer(靜態分析)
打開方式:Xcode->Product-Analyze
然後我們就能看到如下藍色箭頭所示的一些有問題的代碼。
1.6.unrecognized selector send to instancd 快速定位
在debug navigator的斷點欄裏添加Create
Symbolic Breakpoint。
在Symbolic中填寫如下方法簽名:-[NSObject(NSObject) doesNotRecognizeSelector:]
1.7.Signal和EXC_BAD_ACCESS錯誤分析
1.7.1.Signal
在iOS中就是未被捕獲的Objective-C異常(NSException),導致程序向自身發送了SIGABRT信號而崩潰。
就crash而言,SIGABRT是一個比較好解決的,因爲他是一個可掌控的crash。App會在一個目的地終止,因爲系統意識到app做了一些他不能支持的事情。
通常, SIGABRT 異常是由於某個對象接收到未實現的消息引起的。 或者,用簡單的話說,在某個對象上調用了不存在的方法。
下圖爲源碼的運行圖,其中Signal中的Signal(EGV)第一次點擊的時候能彈出alert,如果第二次點擊就沒有崩潰和alert,感覺像卡死一樣。
而Signal(BRT)中的這種信號錯誤多次點擊也是沒有問題的還是能繼續下去。個人猜測可能是SIGSEGV這種信號錯誤會導致了整個進程掛了。
注意:測試的時候如果測試Signal類型的崩潰,不要在xcode下的debug模式進行測試。因爲系統的debug會優先去攔截。應該build好應用之後直接點擊運行app進行測試。
1.7.2.EXC_BAD_ACCESS
2.崩潰類型收集
2.1.新老操作系統兼容
還有就是有些方法是新版操作系統才支持的,而又沒有對該方法是否存在於老系統中做出判斷。這種情況其實還是比較難出現的,除非開發人員太low了,因爲這類方法在xcode編碼時編輯器都會有提醒的。
2.2.本地存儲的數據結構改變
程序在升級時,修改了本地存儲的數據結構,但是對用戶既存的舊數據沒有做好升級,結果導致初始化時因爲無法正確讀取用戶數據而秒退。
第一種:是把服務端傳過來的一些信息保存在本地,使用的時候從本地數據庫取。
然後等我升級了程序,新程序裏model,定義了這個
newId
字段,但是舊版裏面數據已經保存過一遍了沒有這個字段。這時再去取就取不到了。所以後來我就把存儲時解析數據改成了讀取時解析數據。就是不管服務端傳什麼數據都把它存下來,然後在使用的時候再把它解析成對應的model,這樣就不會丟失字段了。
第二種:自己的一些數據存儲在本地SQLlite,新版的時候表結構改了。
SQLlite只支持更改一個表的名字,或者向表中增加一個字段(列),但是我們不能刪除一個已經存在的字段,或者更改一個已經存在的字段的名稱、數據類型、限定符等等。
這種就是有時候新版又添加字段了,或者改變了字段的名稱了。一般來說原有的字段名稱不應該改變,但是添加新字段是常有的事。
一般做法是在第一次創建表的時候加一些冗餘字段,以防後面不時之需。但是如果真沒辦法需要在舊錶上增加新字段了,那就要做數據遷移了。
2.3.訪問的數據爲空或訪問數據類型不對
這類情況是比較常見的,後端傳回了空數據,客戶端沒有做對應的判斷繼續執行下去了,這樣就產生了crash。或者自己本地的某個數據爲空數據而去使用了。還有就是訪問的數據類型不是期望的數據類型而產生崩潰。
服務端都加入默認值,不返回空內容或無key,但是服務端往往會不太願意改,還有就是有些確實應該無值的話key也不用傳,減少數據量的傳輸。
DSCategories這裏面有兩個Category
NSArray+SafeAccessM
和NSDictionary+SafeAccess
可以看一下。
2.4.操作了不該操作的對象,野指針之類
2.4.1野指針介紹
2.4.2野指針崩潰情況
野指針訪問已經釋放的對象crash其實不是必現的,因爲dealloc執行後只是告訴系統,這片內存我不用了,而系統並沒有就讓這片內存不能訪問。
- 對象釋放後內存沒被改動過,原來的內存保存完好,可能不Crash或者出現邏輯錯誤(隨機Crash)。
- 對象釋放後內存沒被改動過,但是它自己析構的時候已經刪掉某些必要的東西,可能不Crash、Crash在訪問依賴的對象比如類成員上、出現邏輯錯誤(隨機Crash)。
- 對象釋放後內存被改動過,寫上了不可訪問的數據,直接就出錯了很可能Crash在objc_msgSend上面(必現Crash,常見)。
- 對象釋放後內存被改動過,寫上了可以訪問的數據,可能不Crash、出現邏輯錯誤、間接訪問到不可訪問的數據(隨機Crash)。
- 對象釋放後內存被改動過,寫上了可以訪問的數據,但是再次訪問的時候執行的代碼把別的數據寫壞了,遇到這種Crash只能哭了(隨機Crash,難度大,概率低)!!
- 對象釋放後再次release(幾乎是必現Crash,但也有例外,很常見)。
2.5.內存處理不當
說到因爲內存處理不當崩潰就要涉及到內存管理問題了。內存管理是軟件開發中一個重要的課題。iOS自從引入ARC機制後,對於內存的管理開發者好像輕鬆了很多,但是還會發生一些內存泄露之類的問題。
對於這一塊知識點需要了解ARC的一些機制,還有用instruments排查內存泄露問題等。這部分我後面會專門寫一篇文章進行闡述。
2.6.主線程UI長時間卡死,被系統殺掉
主線程被卡住是非常常見的場景,具體表現就是程序不響應任何的UI交互。這時按下調試的暫停按鈕,查看堆棧,就可以看到是到底是死鎖、死循環等,導致UI線程被卡住。
2.7.多線程之間切換訪問引起的crash
多線程引起的崩潰大部分是因爲使用數據庫的時候多線程同時讀寫數據庫而造成了crash。
多線程導致的iOS閃退分析這篇文章就是關於多線程crash的調試。