# iOS進階 # 崩潰與日誌分析
在iOS開發中經常需要靠記錄日誌來除錯應用程式、解決崩潰問題等,整理常用的日誌輸出和崩潰日誌分析。
最新更新:2018-11-30
基於CocoaLumberjack 的 ofollow,noindex">Swift使用封裝庫
一、崩潰的捕獲
1、崩潰日誌產生原因
1、應用中有Bug。 2、Watchdog 超時機制 3、使用者強制退出 4、低記憶體終止 5、其他違反系統規則的操作,大部分是記憶體問題 發生崩潰,系統會生成一份崩潰日誌在本地,或者上傳 ITC
2、崩潰的型別(異常、訊號錯誤)
異常類
NSRangeException等 NSException類
訊號錯誤類
訊號中斷(SGIABRT)、非法指令訊號(SIGILL)、匯流排錯誤訊號(SIGBUS)、段錯誤訊號(SIGSEGV)、訪問一個已經釋放的物件(EXC_BAD_ACCESS)
3、捕獲異常崩潰資訊
只能捕獲一些異常崩潰,如 unrecognized selector、NSRangeException beyond bounds越界等Exception屬錯誤
Appdelegate
在Appdelegate 的 didFinishLaunchingWithOptions 中 新增 NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler); 方法實現如下 void UncaughtExceptionHandler(NSException *exception) { /** *獲取異常崩潰資訊 */ NSArray *callStack = [exception callStackSymbols]; NSString *reason = [exception reason]; NSString *name = [exception name]; NSString *content = [NSString stringWithFormat:@"========異常錯誤報告========\nname:%@\nreason:\n%@\ncallStackSymbols:\n%@",name,reason,[callStack componentsJoinedByString:@"\n"]]; //將崩潰資訊持久化在本地,下次程式啟動時、或者後臺,將崩潰資訊作為日誌傳送給開發者。 [[NSUserDefaults standardUserDefaults] setObject:content forKey:@"ExceptionContent"]; }
測試
陣列越界錯誤 NSMutableArray *array = [NSMutableArray array]; NSLog(@"%@",array[1]);
4、捕獲訊號錯誤崩潰資訊
訊號型別崩潰捕獲,測試的時候如果測試Signal型別的崩潰,不要在xcode下的debug模式進行測試。因為系統的debug會優先去攔截。應該build好應用之後直接點選執行app進行測試。
1、什麼是訊號
在電腦科學中,訊號(英語:Signals)是Unix、類Unix以及其他POSIX相容的作業系統中程序間通訊的一種有限制的方式。它是一種非同步的通知機制,用來提醒程序一個事件已經發生。當一個訊號傳送給一個程序,作業系統中斷了程序正常的控制流程,此時,任何非原子操作都將被中斷。如果程序定義了訊號的處理函式,那麼它將被執行,否則就執行預設的處理函式。
在iOS中就是未被捕獲的Objective-C異常(NSException),導致程式向自身傳送了SIGABRT訊號而崩潰。
SIGABRT–程式中止命令中止訊號 SIGALRM–程式超時訊號 SIGFPE–程式浮點異常訊號 SIGILL–程式非法指令訊號 SIGHUP–程式終端中止訊號 SIGINT–程式鍵盤中斷訊號 SIGKILL–程式結束接收中止訊號 SIGTERM–程式kill中止訊號 SIGSTOP–程式鍵盤中止訊號 SIGSEGV–程式無效記憶體中止訊號 SIGBUS–程式記憶體位元組未對齊中止訊號 SIGPIPE–程式Socket傳送失敗中止訊號
2、捕獲方法
Appdelegate 的 didFinishLaunchingWithOptions 中 新增 signal(SIGHUP, SignalHandler); signal(SIGINT, SignalHandler); signal(SIGQUIT, SignalHandler); signal(SIGABRT, SignalHandler); signal(SIGILL, SignalHandler); signal(SIGSEGV, SignalHandler); signal(SIGFPE, SignalHandler); signal(SIGBUS, SignalHandler); signal(SIGPIPE, SignalHandler); 方法實現如下 void SignalHandler(int signal){ int32_t exceptionCount = OSAtomicIncrement32(&UncaughtExceptionCount); if (exceptionCount > UncaughtExceptionMaximum) { return; } void* callstack[128]; int frames = backtrace(callstack, 128); char **strs = backtrace_symbols(callstack, frames); int i; NSMutableArray *backtrace = [NSMutableArray arrayWithCapacity:frames]; for ( i = UncaughtExceptionHandlerSkipAddressCount; i < UncaughtExceptionHandlerSkipAddressCount + UncaughtExceptionHandlerReportAddressCount; i++) { [backtrace addObject:[NSString stringWithUTF8String:strs[i]]]; } free(strs); NSLog(@"%@",backtrace); NSLog(@"%@", [NSString stringWithFormat: NSLocalizedString(@"Signal %d was raised.", nil), signal]); }
3、測試
UIView *tempView = [[UIView alloc]init]; [tempView release]; //物件已經被釋放,記憶體不合法,此塊記憶體地址又沒被覆蓋,所以此記憶體內垃圾記憶體,所以呼叫方法的時候會導致SIGSEGV的錯誤 [tempView setNeedsDisplay]; 或者說 我在堆記憶體中找棧記憶體地址 id x_id = [self performSelector:@selector(createNum)]; - (int)createNum { return 10; } 這種情況也是會導致SIGSEGV的錯誤的 如果在記憶體中釋放不存在的空間,就會導致SIGABRT錯誤 Test * test = {1, 2}; free(test); 記憶體地址不對齊會導致SIGBUS錯誤 char *s = "hello world"; *s = 'H';
4、問題
訊號捕獲後 app卡死了 大部分這型別的錯誤會報錯 EXC_BAD_ACCESS ,而這種錯誤都是發生在記憶體問題,例如 1、訪問資料為空、資料型別不對 2、操作了不該操作的物件,野指標
5、EXC_BAD_ACCESS錯誤的除錯
1、xcode可以用殭屍模式打印出物件 然後通過物件查詢對應的程式碼位置
1、Edit Scheme - Diagnositics - Memory Management 勾選 Zombie Objects 和 Malloc Stack 2、會打印出 cyuyan[7756:17601127] *** -[UIViewController respondsToSelector:]: message sent to deallocated instance 0x7fe71240d390 這句開啟殭屍模式後打出來的輸出,包含了我們需要的 程序pid、崩潰地址,終端通過下面命令檢視堆疊日誌來找到崩潰程式碼 3、查詢日誌 sudo malloc_history 7756 0x7fe71240d390
2、覆寫一個object的respondsToSelector方法
在 other c flags中加入-D FOR_DEBUG(記住請只在Debug Configuration下加入此標記)。這樣當你程式崩潰時,Xcode的console上就會準確地記錄了最後執行的object的方法。重寫一個object的respondsToSelector方法,列印報錯前的 #ifdef _FOR_DEBUG_ -(BOOL) respondsToSelector:(SEL)aSelector { printf("SELECTOR: %s\n", [NSStringFromSelector(aSelector) UTF8String]); return [super respondsToSelector:aSelector]; } #endif
3、通過instruments的Zombies
引申:怎麼定位到野指標的地方。如果還沒定位到,這個物件被提前釋放了,怎麼知道該物件在什麼地方釋放的
一種是多執行緒,一種是野指標。這兩種Crash都帶隨機性,我們要讓隨機crash變成不隨機 把這一隨機的過程變成不隨機的過程。物件釋放後在記憶體上填上不可訪問的資料,其實這種技術其實一直都有,xcode的Enable Scribble就是這個作用。 1、Edit Scheme - Diagnositics - Memory Management 勾選 Malloc Scribble 但是此方法測試後暫時未解決
6、可能崩潰的一些場景
1、野指標
1、物件釋放後記憶體沒被改動過,原來的記憶體儲存完好,可能不Crash或者出現邏輯錯誤(隨機Crash)。 2、物件釋放後記憶體沒被改動過,但是它自己析構的時候已經刪掉某些必要的東西,可能不Crash、Crash在訪問依賴的物件比如類成員上、出現邏輯錯誤(隨機Crash)。 3、物件釋放後記憶體被改動過,寫上了不可訪問的資料,直接就出錯了很可能Crash在objc_msgSend上面(必現Crash,常見)。 4、物件釋放後記憶體被改動過,寫上了可以訪問的資料,可能不Crash、出現邏輯錯誤、間接訪問到不可訪問的資料(隨機Crash)。 5、物件釋放後記憶體被改動過,寫上了可以訪問的資料,但是再次訪問的時候執行的程式碼把別的資料寫壞了,遇到這種Crash只能哭了(隨機Crash,難度大,概率低)!! 6、物件釋放後再次release(幾乎是必現Crash,但也有例外,很常見)。

image
2、記憶體處理不當、記憶體洩露
3、主執行緒UI長時間卡死,系統強制銷燬app
死鎖?
4、多執行緒切換訪問引起的crash
多執行緒搶寫資料庫?
5、和服務端約定的資料結構變更導致操作資料型別問題、或者操作空
二、崩潰日誌的獲取
1、iOS裝置可以直接檢視
路徑: ios 10之後:設定 -> 隱私 -> 分析 -> 資料分析 ios 10之前:設定 -> 隱私 -> 診斷與用量
2、連結裝置到電腦 Itunes同步後,日誌會儲存在電腦上
mac路徑:~/Library/Logs/CrashReporter/MobileDevice/ 可以看到所有和該電腦同步過的裝置的崩潰日誌(.crash檔案) 為什麼有部分crash無法收集到?
3、xcode獲取
xcode檢視裝置日誌並匯出日誌 Window - Devices - 選擇裝置 - 點選View Device Logs -> All logs可以看到所有的崩潰日誌。
4、線上的崩潰,沒有裝置
1、三方:bugly、crashlytics 2、後臺、非同步執行緒將上面描述的捕獲到的崩潰上傳伺服器 3、線上的ITC上可能會有部分日誌,可以通過Xcode同步下來崩潰與能耗日誌 Xcode Window - Organizer - Crashes

image
三、崩潰日誌的解析
1、崩潰日誌的例項
下面是一份測試過程產生的崩潰日誌
//程序資訊 Incident Identifier: 3C3F8BF8-3099-4E82-92E1-8690212E8FF9 CrashReporter Key:bb5f9839ae661ab755f25eff65fee8fd41369628 Hardware Model:iPod5,1 Process:demo [973] Path:/private/var/containers/Bundle/Application/0D3657DE-DE1E-4FF0-A0F7-C09EBC002763/demo.app/demo Identifier:com.yanghuang.demo Version:17 (1.1.9) Code Type:ARM (Native) Parent Process:launchd [1] //基本資訊 Date/Time:2017-08-22 16:11:49.49 +0800 Launch Time:2017-08-22 16:11:40.40 +0800 OS Version:iOS 9.3.5 (13G36) Report Version:104 //異常 Exception Type:EXC_BREAKPOINT (SIGTRAP) Exception Codes: 0x0000000000000001, 0x00000000e7ffdefe Triggered by Thread:0 Filtered syslog: None found //執行緒回溯 Thread 0 name:Dispatch queue: com.apple.main-thread Thread 0 Crashed: 0libswiftCore.dylib0x0033788c 0x1ac000 + 1620108 1...wiftSwiftOnoneSupport.dylib0x009b4830 0x9ac000 + 34864 2demo0x00029288 0x24000 + 21128 3demo0x00029414 0x24000 + 21524 4UIKit0x25cd2754 0x25c87000 + 309076 5UIKit0x25cd26e0 0x25c87000 + 308960 6UIKit0x25cba6d2 0x25c87000 + 210642 7UIKit0x25cd2004 0x25c87000 + 307204 8UIKit0x25cd1c7e 0x25c87000 + 306302 9UIKit0x25cca68e 0x25c87000 + 276110 10UIKit0x25c9b124 0x25c87000 + 82212 11UIKit0x25c996d2 0x25c87000 + 75474 12CoreFoundation0x216e1dfe 0x21626000 + 769534 13CoreFoundation0x216e19ec 0x21626000 + 768492 14CoreFoundation0x216dfd5a 0x21626000 + 761178 15CoreFoundation0x2162f228 0x21626000 + 37416 16CoreFoundation0x2162f014 0x21626000 + 36884 17GraphicsServices0x22c1fac8 0x22c16000 + 39624 18UIKit0x25d03188 0x25c87000 + 508296 19demo0x0002ff48 0x24000 + 48968 20libdyld.dylib0x212d7872 0x212d5000 + 10354 Thread 1 name:Dispatch queue: com.apple.libdispatch-manager Thread 1: 0libsystem_kernel.dylib0x213ac2f8 0x21396000 + 90872 1libdispatch.dylib0x212a1d60 0x2128b000 + 93536 2libdispatch.dylib0x212a1abe 0x2128b000 + 92862 ... 省略部分內容 //二進位制映像 Binary Images 0x24000 - 0x33fff demo armv7<aa31c8c1f8cb333596dbfe056b120673> /var/containers/Bundle/Application/0D3657DE-DE1E-4FF0-A0F7-C09EBC002763/demo.app/demo 0x140000 - 0x15bfff Masonry armv7<9615e97c54d335f7821568396c65d324> /var/containers/Bundle/Application/0D3657DE-DE1E-4FF0-A0F7-C09EBC002763/demo.app/Frameworks/Masonry.framework/Masonry
1.程序資訊
第一部分是閃退程序的相關資訊。 Incident Identifier 是崩潰報告的唯一識別符號。 CrashReporter Key 是與裝置標識相對應的唯一鍵值。雖然它不是真正的裝置識別符號,但也是一個非常有用的情報:如果你看到100個崩潰日誌的CrashReporter Key值都是相同的,或者只有少數幾個不同的CrashReport值,說明這不是一個普遍的問題,只發生在一個或少數幾個裝置上。 Hardware Model 標識裝置型別。 如果很多崩潰日誌都是來自相同的裝置型別,說明應用只在某特定型別的裝置上有問題。上面的日誌裡,崩潰日誌產生的裝置是iPhone 4s。 Process 是應用名稱。中括號裡面的數字是閃退時應用的程序ID。
2.基本資訊
這部分給出了一些基本資訊,包括閃退發生的日期和時間,裝置的iOS版本。如果有很多崩潰日誌都來自iOS 6.0,說明問題只發生在iOS 6.0上。 Version: 崩潰程序的版本號. 這個值包含在 CFBundleVersion and CFBundleVersionString中. Code Type: 崩潰日誌所在裝置的架構. 會是ARM-64, ARM, x86-64, or x86中的一個. OS Version: 崩潰發生時的系統版本
3.異常資訊
異常資訊會列出異常的型別、位置。 在這部分,你可以看到閃退發生時丟擲的異常型別。還能看到異常編碼和丟擲異常的執行緒。根據崩潰報告型別的不同,在這部分你還能看到一些另外的資訊。 Exception Codes: 處理器的具體資訊有關的異常編碼成一個或多個64位進位制數。通常情況下,這個區域不會被呈現,因為將異常程式碼解析成人們可以看懂的描述是在其它區域進行的。 Exception Subtype: 供人們可讀的異常程式碼的名字 Exception Message: 從異常程式碼中提取的額外的可供人們閱讀的資訊. Exception Note: 不是特定於一個異常型別的額外資訊.如果這個區域包含SIMULATED (這不是一個崩潰)然後程序沒有崩潰,但是被watchdog殺掉了 Termination Reason: 當一個程序被終止的時的原因。 Triggered by Thread: 異常所在的執行緒.
4.執行緒回溯
這部分提供應用中所有執行緒的回溯日誌。 回溯是閃退發生時所有活動幀清單。它包含閃退發生時呼叫函式的清單。看下面這行日誌: 2demo0x00029288 0x24000 + 21128 它包括四列: 幀編號—— 此處是2。 二進位制庫的名稱 ——此處是 demo. 呼叫方法的地址 ——此處是 0x00029288. 第四列分為兩個子列,一個基本地址和一個偏移量。此處是0×0x24000 + 21128, 第一個數字指向檔案,第二個數字指向檔案中的程式碼行。
5.二進位制映像
這部分列出了閃退時已經載入的二進位制檔案。
2、符號化Symbolication

image
第一次看到崩潰日誌上的回溯時,你或許會覺得它沒什麼意義。我們習慣使用方法名和行數,而非像這樣的神祕位置:
2demo0x00029288 0x24000 + 21128
將這些十六進位制地址轉化成方法名稱和行數的過程稱之為符號化。
從Xcode的Organizer視窗獲取崩潰日誌後過幾秒鐘,崩潰日誌將被自動符號化。上面那行被符號化後的版本如下 :
2demo0x00029288 ViewController.crashAction(Any) -> () (ViewController.swift:36)
Xcode符號化崩潰日誌時,需要訪問與App Store上對應的應用二進位制檔案以及生成二進位制檔案時產生的 .dSYM 檔案。必需完全匹配才行。否則,日誌將無法被完全符號化。
所以,保留每個分發給使用者的編譯版本非常重要。提交應用前進行歸檔時,Xcode將儲存應用的二進位制檔案。可以在Xcode Organizer的Archives標籤欄下找到所有已歸檔的應用檔案。
在發現崩潰日誌時,如果有相匹配的.dSYM檔案和應用二進位制檔案,Xcode會自動對崩潰日誌進行符號化。如果你換到別的電腦或建立新的賬戶,務必將所有二進位制檔案移動到正確的位置,使Xcode能找到它們。
注意:
1、你必需同時保留應用二進位制檔案和.dSYM檔案才能將崩潰日誌完整符號化。每次提交到iTunes Connect的構建都必需歸檔。 .dSYM檔案和二進位制檔案是特定綁定於每一次構建和後續構建的,即使來自相同的原始碼檔案,每一次構建也與其他構建不同,不能相互替換。如果你使用Build 和 Archive 命令,這些檔案會自動放在適當位置。 如果不是使用Build 和 Archive命令,放在Spotlight能夠搜尋到的位置(比如Home目錄)即可。
2、xcode debug方式打包預設沒有DSYM檔案,只需要修改對應的build options即可
build settings -> build options
把debug 項改成 DWARF with dSYM File 即可
如何通過.crash檔案反編譯得到明文的crash檔案
需要檔案:
1、demo.app 2、demo.app.dSYM 3、demo.crash (已獲得) 4、symbolicatecrash 符號化前先檢查一下三者的uuid是不是一致的,只有是一致的才能符號化成功。 檢視xx.app檔案的uuid的方法: dwarfdump --uuid xxx.app/xxx (xxx工程名) 檢視xx.app.dSYM檔案的uuid的方法令: dwarfdump --uuid xxx.app.dSYM (xxx工程名) 而.crash的uuid位於,crash日誌中的Binary Images:中的第一行尖括號內。如:armv7<8bdeaf1a0b233ac199728c2a0ebb4165>
步驟如下:
1、首先我們找到Archives目錄(/Users/使用者名稱/Library/Developer/Xcode/Archives/2017-08-22/demo)
2、找到對應app目錄、對應的Archives檔案、顯示包內容開啟。在dSYMs資料夾中找到demo.app.dSYM
在Products->Applications資料夾中找到 demo.app
3、找到symbolicatecrash
find /Applications/Xcode.app -name symbolicatecrash -type f //終端輸入上面命令、得到一個路徑,這個路徑就是symbolicatecrash的路徑 拷貝到和上面檔案同一目錄
3: 在終端中輸入以下命令
./symbolicatecrash -v demo.crash demo.app.dSYM 如果出現Error: "DEVELOPER_DIR" is not defined 再執行下面一句後再次執行 export DEVELOPER_DIR="/Applications/XCode.app/Contents/Developer"
然後用控制檯開啟你的demo.crash檔案, 你就會看到編譯後的crash檔案, 同Xcode看到的崩潰日誌一致。通過檢視崩潰日誌,可以輕易的找到崩潰原因並修正。
Thread 0 name:Dispatch queue: com.apple.main-thread Thread 0 Crashed: 0libswiftCore.dylib0x0033788c 0x1ac000 + 1620108 1...wiftSwiftOnoneSupport.dylib0x009b4830 0x9ac000 + 34864 2demo0x00029288 ViewController.crashAction(Any) -> () (ViewController.swift:36) 3demo0x00029414 @objc ViewController.crashAction(Any) -> () (ViewController.swift:0) 4UIKit0x25cd2754 -[UIApplication sendAction:to:from:forEvent:] + 80 5UIKit0x25cd26e0 -[UIControl sendAction:to:forEvent:] + 64 6UIKit0x25cba6d2 -[UIControl _sendActionsForEvents:withEvent:] + 466 7UIKit0x25cd2004 -[UIControl touchesEnded:withEvent:] + 604 8UIKit0x25cd1c7e -[UIWindow _sendTouchesForEvent:] + 646 9UIKit0x25cca68e -[UIWindow sendEvent:] + 642 10UIKit0x25c9b124 -[UIApplication sendEvent:] + 204 11UIKit0x25c996d2 _UIApplicationHandleEventQueue + 5010 12CoreFoundation0x216e1dfe __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 14 13CoreFoundation0x216e19ec __CFRunLoopDoSources0 + 452 14CoreFoundation0x216dfd5a __CFRunLoopRun + 794 15CoreFoundation0x2162f228 CFRunLoopRunSpecific + 520 16CoreFoundation0x2162f014 CFRunLoopRunInMode + 108 17GraphicsServices0x22c1fac8 GSEventRunModal + 160 18UIKit0x25d03188 UIApplicationMain + 144 19demo0x0002ff48 main (AppDelegate.swift:13) 20libdyld.dylib0x212d7872 start + 2
3、低記憶體閃退
因為低記憶體崩潰日誌與普通崩潰日誌略有不同。
iOS裝置檢測到低記憶體時,虛擬記憶體系統發出通知請求應用釋放記憶體。這些通知傳送到所有正在執行的應用和程序,試圖收回一些記憶體。
如果記憶體使用依然居高不下,系統將會終止後臺執行緒以緩解記憶體壓力。如果可用記憶體足夠,應用將能夠繼續執行而不會產生崩潰報告。否則,應用將被iOS終止,併產生低記憶體崩潰報告。
低記憶體崩潰日誌上沒有應用執行緒的堆疊回溯。相反,上面顯示的是以記憶體頁數為單位的各程序記憶體使用量。
被iOS因釋放記憶體頁終止的程序名稱後面你會看到jettisoned 字樣。如果看到它出現在你的應用名稱後面,說明你的應用因使用太多記憶體而被終止了。
低記憶體崩潰日誌看起來像這樣:

img
4、異常編碼
通常,異常編碼以一些文字開頭,緊接著是一個或多個十六進位制值,此數值正是說明閃退根本性質的所在。 從這些編碼中,可以區分出閃退是因為程式錯誤、非法記憶體訪問或者是其他原因。
下面是一些常見的異常編碼:
0x8badf00d: 該編碼表示應用是因為發生watchdog超時而被iOS終止的。通常是應用花費太多時間而無法啟動、終止或響應用系統事件。 0xbad22222: 該編碼表示 VoIP 應用因為過於頻繁重啟而被終止。 0xdead10cc: 該程式碼表明應用因為在後臺執行時佔用系統資源,如通訊錄資料庫不釋放而被終止 。 0xdeadfa11: 該程式碼表示應用是被使用者強制退出的。根據蘋果文件, 強制退出發生在使用者長按開關按鈕直到出現 “滑動來關機”, 然後長按 Home按鈕。強制退出將產生 包含0xdeadfa11 異常編碼的崩潰日誌, 因為大多數是強制退出是因為應用阻塞了介面。 EXC_CRASH // SIGABRT: 程序異常退出。該異常型別崩潰最常見的原因是未捕獲的Objective-C和C++異常和呼叫abort()。如果他們需要太多的時間來初始化,程式將被終止,因為觸發了看門狗。如果是因為啟動的時候被掛起,所產生的崩潰報告異常型別(Exception Subtype)將是launch_hang。 EXC_BREAKPOINT // SIGTRAP:程序試圖執行非法或未定義指令。這個程序可能試圖通過一個配置錯誤的函式指標,跳到一個無效的地址。 SIGSEGV、SIGBUS 這些在前面捕獲異常的內容有描述
注意:在後臺任務列表中關閉已掛起的應用不會產生崩潰日誌。 一旦應用被掛起,它何時被終止都是合理的。所以不會產生崩潰日誌。
四、線上問題的處理
思考:
1、工作中是否有遇到過線上反饋回來的問題,你是如何定位問題處理的
2、如果有特殊賬戶才出現的問題,但是又拿不到使用者賬戶,該如何處理