iOS Crash問題

本文就捕獲iOS Crash、Crash日志組成、Crash日志符號(hào)化、異常信息解讀、常見的Crash五部分介紹。

一、捕獲iOS Crash

1、設(shè)置異常斷點(diǎn)并運(yùn)行


說明:設(shè)置Xcode異常斷點(diǎn)后運(yùn)行程序,發(fā)生Crash時(shí),斷點(diǎn)會(huì)定位到出錯(cuò)的代碼行,但僅適用于開發(fā)階段。線上APP的Crash還需要通過收集Crash機(jī)制來捕獲Crash并記錄在日志中。

2、Mach異常 和 Unix信號(hào)

iOS Crash發(fā)生時(shí),先產(chǎn)生Mach異常(最底層的內(nèi)核級(jí)異常),然后Mach異常在host層被ux_exception轉(zhuǎn)換為相應(yīng)的Unix信號(hào),并通過threadsignal將信號(hào)投遞到出錯(cuò)的線程。

在捕獲Crash事件時(shí),優(yōu)選Mach異常。因?yàn)镸ach異常處理會(huì)先于Unix信號(hào)處理發(fā)生,如果Mach異常的handler讓程序exit了,那么Unix信號(hào)就永遠(yuǎn)不會(huì)到達(dá)這個(gè)進(jìn)程了。而轉(zhuǎn)換Unix信號(hào)是為了兼容更為流行的POSIX標(biāo)準(zhǔn)(SUS規(guī)范),這樣就不必了解Mach內(nèi)核也可以通過Unix信號(hào)的方式來兼容開發(fā)。

在方案實(shí)現(xiàn)時(shí),通過捕獲Mach異常+Unix信號(hào)組合方式來捕獲Crash事件。在選擇具體方案時(shí),可以選擇PLCrashReporter這樣優(yōu)秀的開源項(xiàng)目,也可以選擇友盟、Bugly 這類完善的Crash上報(bào)和統(tǒng)計(jì)的產(chǎn)品(試項(xiàng)目需求而定)。

3、捕獲Crash

并不是所有的Crash都可以捕獲到NSException,如果捕獲不到,可以使用signal機(jī)制來捕獲Crash發(fā)生時(shí)的錯(cuò)誤內(nèi)容。

1) 可以捕獲的NSException,通過注冊(cè)NSUncaughtExceptionHandler捕獲異常信息

//注冊(cè)異常處理函數(shù)

NSSetUncaughtExceptionHandler(&uncaught_exception_handler);

//異常處理函數(shù)

static void uncaught_exception_handler (NSException *exception) {

//可以取到 NSException 信息

//...

abort();

}

說明: 使用Objective-C的異常處理是不能得到signal的。

2) 無法捕獲的NSException,利用Unix標(biāo)準(zhǔn)的signal機(jī)制,注冊(cè)SIGABRT, SIGBUS, SIGSEGV等信號(hào)發(fā)生時(shí)的處理函數(shù)。

//注冊(cè)處理SIGSEGV信號(hào)

signal(SIGSEGV,handleSignal);

// 注冊(cè)處理其他信號(hào) ....

//信號(hào)處理函數(shù)

static void handleSignal( int sig ) {

}

二、Crash日志組成

上部分介紹了Crash的捕獲,這部分來看看Crash日志的組成。

1、日志內(nèi)容Demo

日志主要分為六個(gè)部分:進(jìn)程信息、基本信息、異常信息、線程回溯、線程狀態(tài)和二進(jìn)制映像。下面是從某APP具體的Crash日志抽出的主要信息,展示如下:

//注冊(cè)處理SIGSEGV信號(hào)

//1、進(jìn)程信息

Hardware Model: iPhone9,2

Process: AppName [3580]

Path: /var/containers/Bundle/Application/C7B90C8A-E269-4413-A011-552971D1ED39/AppName.app

Identifier: xxxx.xxx.xxxx.xxx

Version: xx.xx

Code Type: ARM-64 (Native)

Parent Process:? [1]

//2、基本信息

Date/Time: 2017-05-22 03:05:06.743 +0800

OS Version: iPhone OS 10.2.1 (14D27)

//3、異常信息

Exception Type: NSInvalidArgumentException(SIGABRT)

Exception Codes: -[NSNull integerValue]: unrecognized selector sent to instance 0x1a9d88ef8 at 0x00000001835c7014

Crashed Thread: 0

//4、線程回溯 (展示發(fā)生Crash線程的回溯信息,其他略)

Thread 0 Crashed:

0? libsystem_kernel.dylib???????? 0x00000001835c7014 __pthread_kill + 4

1? libsystem_c.dylib????????????? 0x000000018353b400 abort + 140

2? AppName???????????????????????? 0x0000000100a26704 0x0000000100028000 + 10479360

3? CoreFoundation???????????????? 0x00000001845f9538 ___handleUncaughtException +? 644

2? CoreFoundation???????????????? 0x0000000184600268 ___methodDescriptionForSelector

3? CoreFoundation???????????????? 0x00000001845fd270 ____forwarding___ +? 916

4? CoreFoundation???????????????? 0x00000001844f680c _CF_forwarding_prep_0 + 80

5? AppName???????????????????????? 0x0000000100205280 0x0000000100028000 + 1954432

6? AppName???????????????????????? 0x00000001002ae59c 0x0000000100028000 + 2647440

7? AppName???????????????????????? 0x0000000100482944 0x0000000100028000 + 4565312

16 CoreFoundation???????????????? 0x00000001845a6810 ___CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ +? 12

+? 12

17 CoreFoundation???????????????? 0x00000001845a43fc ___CFRunLoopRun +? 1660

18 CoreFoundation???????????????? 0x00000001844d22b8 CFRunLoopRunSpecific + 436

//5、進(jìn)程狀態(tài)(展示部分)

Thread 0 crashed with ARM 64 Thread State:

x0:? 000000000000000000??? x1: 000000000000000000??? x2: 000000000000000000???? x3: 0xffffffffffffffff

x4:? 0x0000000000000010??? x5: 0x0000000000000020??? x6: 000000000000000000???? x7: 000000000000000000

x8:? 0x0000000008000000??? x9: 0x0000000004000000?? x10: 000000000000000000??? x11: 0x00000001ac336c83

x12: 0x00000001ac336c83??? x13: 0x0000000000000018?? x14: 0x0000000000000001??? x15: 0x0000000000000881

x16: 0x0000000000000148??? x17: 000000000000000000?? x18: 000000000000000000??? x19: 0x0000000000000006

//6、二進(jìn)制映像 (展示部分)

Binary Images:

0x100028000 - 0x1011dbfff +AppName arm64 /var/containers/Bundle/Application/C7B90C8A-E269-4413-A011-552971D1ED39/AppName.app/AppName

0x18368a000 - 0x183693fff? libsystem_pthread.dylib arm64 <258dc0c51499393bba7ba3e83dc5bfbb> /usr/lib/system/libsystem_pthread.dylib

0x1835a8000 - 0x1835ccfff? libsystem_kernel.dylib arm64 <1baa3f5629c43467879d4cf463a20b06> /usr/lib/system/libsystem_kernel.dylib

0x1834b1000 - 0x1834b5fff? libdyld.dylib arm64 /usr/lib/system/libdyld.dylib

0x1834d8000 - 0x183556fff? libsystem_c.dylib arm64 <8a5a190d70563f3c8d4ce16cab74f599> /usr/lib/system/libsystem_c.dylib

0x183481000 - 0x1834b0fff? libdispatch.dylib arm64 /usr/lib/system/libdispatch.dylib

0x183028000 - 0x183401fff? libobjc.A.dylib arm64 <538f809dcd7c35ceb59d99802248f045> /usr/lib/libobjc.A.dylib

2、日志內(nèi)容組成分析

整個(gè)日志內(nèi)容中,直接和Crash信息相關(guān),最能幫助開發(fā)者定位問題部分是: 異常信息 和 線程回溯部分的內(nèi)容。

1) 進(jìn)程信息:發(fā)生Crash閃退進(jìn)程的相關(guān)信息

Hardware Model : 標(biāo)識(shí)設(shè)備類型。 如果很多崩潰日志都是來自相同的設(shè)備類型,說明應(yīng)用只在某特定類型的設(shè)備上有問題。上面的日志里,崩潰日志產(chǎn)生的設(shè)備是iPhone 7 Plus (iPhone 7 Plus 也是2個(gè)版本 iPhone9,2 和 iPhone9,4. 硬件代號(hào)為 D11AP 和 D111AP. 型號(hào)有: A1661, A1784, A1785 和 A1786. )

Process 是應(yīng)用名稱。中括號(hào)里面的數(shù)字是閃退時(shí)應(yīng)用的進(jìn)程ID。

2) 基本信息:給出了一些基本信息,包括閃退發(fā)生的日期和時(shí)間,設(shè)備的iOS版本。

3) 異常信息:閃退發(fā)生時(shí)拋出的異常類型。還能看到異常編碼和拋出異常的線程。

//以上面內(nèi)容中的異常信息為例:

Exception Type: NSInvalidArgumentException(SIGABRT)

Exception Codes: -[NSNull integerValue]: unrecognized selector sent to instance 0x1a9d88ef8 at 0x00000001835c7014

Crashed Thread: 0

Exception Type異常類型:通常包含1.7中的Signal信號(hào)和EXC_BAD_ACCESS,NSRangeException等。

Exception Codes:異常編碼:

Crashed Thread:發(fā)生Crash的線程id

4) 線程回溯:回溯是閃退發(fā)生時(shí)所有活動(dòng)幀清單。它包含閃退發(fā)生時(shí)調(diào)用函數(shù)的清單。

5) 線程狀態(tài):閃退時(shí)寄存器中的值。一般不需要這部分的信息,因?yàn)榛厮莶糠值男畔⒁呀?jīng)足夠讓你找出問題所在。

6) 二進(jìn)制映像:閃退時(shí)已經(jīng)加載的二進(jìn)制文件。

三、異常信息解讀

1、Exception Type(異常類型)

Exception Type:通常包含Signal信號(hào) 和 EXC_BAD_ACCESS,NSRangeException等。

具體信號(hào)說明參見iOS異常捕獲

2、Exception Code(異常編碼)

Exception Code:以一些文字開頭,緊接著是一個(gè)或多個(gè)十六進(jìn)制值。這些數(shù)值說明了Crash發(fā)生的本質(zhì)。

從Exception Code中,可以區(qū)分出Crash是因?yàn)槌绦蝈e(cuò)誤、非法內(nèi)存訪問還是其他原因。常見的異常編碼如下表:

說明1:詳細(xì)的異常編碼代表的含義請(qǐng)參考:Hexspeak

說明2:在后臺(tái)任務(wù)列表中關(guān)閉已掛起的應(yīng)用不會(huì)產(chǎn)生崩潰日志。 因?yàn)閼?yīng)用一旦被掛起,它何時(shí)被終止都是合理的。所以不會(huì)產(chǎn)生崩潰日志。

四、Crash日志符號(hào)化

1、概述

線程回溯部分內(nèi)容如下:

5? AppName???????????????????????? 0x0000000100205280 0x0000000100028000 + 1954432

6? AppName???????????????????????? 0x00000001002ae59c 0x0000000100028000 + 2647440

這兩條記錄包括四列:(以第一條記錄為例子)

幀編號(hào)—— 5(數(shù)字越小,發(fā)生時(shí)間越晚,發(fā)生順序越往后,越好鎖定問題的范圍)

二進(jìn)制庫的名稱 ——此處是 AppName.

調(diào)用方法的地址 ——此處是 0x0000000100205280.

第四列分為兩個(gè)子列,一個(gè)基本地址和一個(gè)偏移量。此處是 x0000000100028000 + 1954432, 第一個(gè)數(shù)字指向文件,第二個(gè)數(shù)字指向文件中的代碼行。

說明1:線程回溯部分并不是我們習(xí)慣使用方法名和行數(shù),而是十六進(jìn)制地址。所以我們?cè)诜治鯟rash前需要將這些十六進(jìn)制地址轉(zhuǎn)化成方法名稱和行數(shù),改過程被稱為符號(hào)化。

說明2:符號(hào)化Crash日志需要獲取對(duì)應(yīng)的應(yīng)用二進(jìn)制文件以及生成二進(jìn)制文件時(shí)產(chǎn)生的 .dSYM 文件(符號(hào)表)。必需完全匹配才行。否則,日志將無法被完全符號(hào)化。

說明3: Xcode編譯項(xiàng)目后,會(huì)得到同名的 dSYM 文件(符號(hào)表),dSYM 文件(符號(hào)表)是保存 16 進(jìn)制函數(shù)地址映射信息的中轉(zhuǎn)文件,我們調(diào)試的 symbols 都會(huì)包含在這個(gè)文件中,并且每次編譯項(xiàng)目的時(shí)候都會(huì)生成一個(gè)新的 dSYM 文件,位于 /Users/<用戶名>/Library/Developer/Xcode/Archives 目錄下,對(duì)于每一個(gè)發(fā)布版本我們都很有必要保存對(duì)應(yīng)的 Archives 文件。

說明4:符號(hào)化可以使用Xcode的兩種命令 symbolicatecrash命令 + atos命令

2、symbolicatecrash命令

1)首選找到symbolicatecrash命令的位置

find /Applications -name symbolicatecrash -type f

//我的本機(jī)命令的位置:/Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash

2)找到線上版本對(duì)應(yīng)的xcarchive文件。從中找到.dSYM和.app文件

xcarchive所在的路徑一般在: /Users/<用戶名>/Library/Developer/Xcode/Archives 目錄下

3)獲取crash日志文件

線上App的Crash日志經(jīng)由Crash日志收集服務(wù)獲得(主要來源)。

也可以從真機(jī)上獲取Crash日志文件。點(diǎn)擊Window -> Devices,選擇你自己的機(jī)器,然后點(diǎn)擊View Device Logs,右鍵可以導(dǎo)出Crash文件。

獲取的這些日志文件都需要符號(hào)化處理。

4)將symbolicatecrash、.dSYM、.app、crash.crash拷貝到桌面下同一個(gè)文件夾下

5)檢查 xx.app 和 xx.app.dSYM 文件以及crash 文件這三種的 UUID是否一致。

查看 xx.app 文件的 UUID,terminal 中輸入命令 :

dwarfdump --uuid xx.app/xx (xx代表你的項(xiàng)目名)

查看 xx.app.dSYM 文件的 UUID ,在 terminal 中輸入命令:

dwarfdump --uuid xx.app.dSYM

查看crash 日志中的Incident Identifier (crash 文件的 UUID)

6)使用命令,生成“可定位問題的crash文件”

dwarfdump --uuid xx.app.dSYM

//symbolreportXXX.crash就是符號(hào)化后的文件

./symbolicatecrash crashXXX.crash appName.app.dSYM > symbolreportXXX.crash

7) 根據(jù)符號(hào)化后的線程回溯信息,可以幫助定位出問題的代碼行。

說明:如果執(zhí)行symbolicatecrash命令出現(xiàn) Error: "DEVELOPER_DIR" is not defined at ./symbolicatecrash...這樣的錯(cuò)誤,可以在執(zhí)行命令前,輸入export DEVELOPER_DIR="/Applications/XCode.app/Contents/Developer"

3、atos命令

在符號(hào)化時(shí)候,還可以使用atos命令。發(fā)現(xiàn)armv7處理器上的crash使用symbolicatecrash無法符號(hào)化。

1)將.dSYM、.app、crash.crash放到同一個(gè)文件夾下。

2) 知道crash文件的UUID:執(zhí)行g(shù)rep "AppName arm" *crash,得到結(jié)果

crash1.crash:0x100040000 - 0x100e23fff +AppName arm64 /var/containers/Bundle/Application/55A4D641-847F-4D24-86E1-129B28461858/AppName.app/AppName

crash2.crash:0x100060000 - 0x100e43fff +AppName arm64 /var/containers/Bundle/Application/3229ED68-8D19-406D-A3F5-EC0310C9DB7C/QAppName.app/AppName

crash3.crash:??? 0x5000 -?? 0xce8fff +AppName armv7 <7d62327effef37d384658020625a9944> /var/containers/Bundle/Application/C6BE271D-2EAC-42C0-8E72-4523F88C76B2/AppName.app/AppName

其中0x100040000、0x100060000、0x5000是加載地址(loadingAddress), 而arm64、armv7 是 architecture 的值(architectureValue),這兩個(gè)值后面都要用。

3)然后執(zhí)行atos命令,輸入成功,進(jìn)入待輸入狀態(tài)

xcrun atos -o appName.app.dSYM/Contents/Resources/DWARF/appName -l loadingAddress -arch architectureValue

4) 此時(shí)輸入App對(duì)應(yīng)的Crash地址,得到發(fā)生crash的信息。

實(shí)例1:

grep "AppName arm" *crash

xcrun atos -o AppName.app.dSYM/Contents/Resources/DWARF/AppName -l 0x100040000 -arch arm64

實(shí)例2:

grep "AppName arm" *crash

xcrun atos -o AppName.app.dSYM/Contents/Resources/DWARF/AppName -l 0x5000 -arch armv7

五、常見的Crash

有一些Crash比較常見,下面羅列出5種常見的Crash。

1、數(shù)組操作

場(chǎng)景1:取數(shù)據(jù)索引越界。一般發(fā)生在UITableView的使用中,因?yàn)閏ellForRowAtIndexPath代理方法是異步執(zhí)行的,UITableView對(duì)象的dataSource一旦在加載數(shù)據(jù)過程中發(fā)生變化,極有可能發(fā)生數(shù)組越界的異常。在多線程場(chǎng)景下,列表界面的數(shù)據(jù)有可能經(jīng)常變化,很可能發(fā)生;當(dāng)列表界面數(shù)據(jù)不怎么變化的時(shí)候,幾乎感知不到這種異常的存在。

解決辦法:從數(shù)組中取數(shù)據(jù)前,校驗(yàn)索引是否正確。

@implementation NSMutableArray (Safe)

- (id)safeObjectAtIndex:(NSUInteger)index{

if (index < self.count){

return [self objectAtIndex:index];

}else{

NSLog(@"警告:數(shù)組越界!!!");

}

return nil;

}

@end

場(chǎng)景2:數(shù)組添加數(shù)據(jù)對(duì)象時(shí)nil

解決辦法:添加對(duì)象到數(shù)組前,判斷是否是nil

說明:數(shù)組的刪除等操作處理類似,數(shù)組操作前要進(jìn)行數(shù)據(jù)校驗(yàn)。

2、多線程下的Crash

一般多線程發(fā)生的Crash,會(huì)收到SIGSEGV信號(hào),表明試圖訪問未分配給自己的內(nèi)存, 或試圖往沒有寫權(quán)限的內(nèi)存地址寫數(shù)據(jù)。

場(chǎng)景1:子線程中更新UI

解決辦法:將UI更新操作放在主線程中,可以使用performSelectorOnMainThread 或 GCD

//子線程中,使用宏將更新UI的任務(wù)派發(fā)到主隊(duì)列

#define dispatch_main_sync_safe(block) \

if ([NSThread isMainThread]) { \

block(); \

} else { \

dispatch_sync(dispatch_get_main_queue(), block); \

}

#define dispatch_async_main(block) ? ? ? ? ? ? ?dispatch_async(dispatch_get_main_queue(), block)

場(chǎng)景2:多線程中創(chuàng)建單例

解決辦法:使用dispatch_once,保證代碼只執(zhí)行一次,保證線程安全。

//以QSAccountManager單例為例

static QSAccountManager *_shareManager = nil;

+ (instancetype)shareManager{

static dispatch_once_t once;

dispatch_once(&once, ^{

_shareManager = [[self alloc] init];

});

return _shareManager;

}

+ (instancetype)allocWithZone:(struct _NSZone *)zone{

static dispatch_once_t onceToken;

dispatch_once(&onceToken, ^{

_shareManager = [super allocWithZone:zone];

});

return _shareManager;

}

- (nonnull id)copyWithZone:(nullable NSZone *)zone{

return _shareManager;

}

場(chǎng)景3:多線程下非線程安全類的使用,如NSMutableArray、NSMutableDictionary

解決辦法:使用派發(fā)隊(duì)列或鎖保證數(shù)據(jù)讀寫安全。具體實(shí)現(xiàn)詳見 iOS實(shí)錄12:NSMutableArray使用中忽視的問題中第一部分。

場(chǎng)景4:數(shù)據(jù)緩存到磁盤和讀取。

解決辦法:使用派發(fā)隊(duì)列或鎖保證數(shù)據(jù)讀寫安全。如將數(shù)據(jù)的讀取和寫異步放入串行同步隊(duì)列,保證數(shù)據(jù)同步,線程安全。

3、WatchDog 超時(shí)造成的Crash

一般異常編碼是0x8badf00d ,表示應(yīng)用是因?yàn)榘l(fā)生watchdog超時(shí)而被iOS終止的。通常是應(yīng)用花費(fèi)太多時(shí)間而無法啟動(dòng)、終止或響應(yīng)用系統(tǒng)事件。

場(chǎng)景1:主線程中執(zhí)行耗時(shí)的操作,導(dǎo)致主線程被卡超過一定的時(shí)間。

解決辦法:主線程中只負(fù)責(zé)UI的更新和響應(yīng),將耗時(shí)的操作采用異步的方式放到后臺(tái)線程執(zhí)行。耗時(shí)操作包括:網(wǎng)絡(luò)請(qǐng)求,數(shù)據(jù)庫讀寫等。

4、performSelector:withObject:afterDelay下的Crash

場(chǎng)景1:對(duì)象釋放比performSelector:afterDelay要早

解決辦法:在對(duì)應(yīng)類的dealloc中執(zhí)行cancelPreviousPerformRequestsWithTarget取消執(zhí)行。

5、SIGPIPE導(dǎo)致的程序退出

當(dāng)服務(wù)器close一個(gè)連接時(shí),若client端接著發(fā)數(shù)據(jù)。根據(jù)TCP協(xié)議的規(guī)定,會(huì)收到一個(gè)RST響應(yīng),client再往這個(gè)服務(wù)器發(fā)送數(shù)據(jù)時(shí),系統(tǒng)會(huì)發(fā)出一個(gè)SIGPIPE信號(hào)給進(jìn)程,告訴進(jìn)程這個(gè)連接已經(jīng)斷開了,不要再寫了。而根據(jù)信號(hào)的默認(rèn)處理規(guī)則,SIGPIPE信號(hào)的默認(rèn)執(zhí)行動(dòng)作是terminate(終止、退出),所以client會(huì)退出。

場(chǎng)景:長連接socket或重定向管道進(jìn)入后臺(tái),沒有關(guān)閉

解決辦法1:切換到后臺(tái)時(shí),關(guān)閉長連接和管道,回到前臺(tái)再重建;

解決辦法2:使用signal(SIGPIPE,SIG_IGN),將SIGPIPE交給了系統(tǒng)處理。這么做將SIGPIPE設(shè)為SIG_IGN,使得客戶端不執(zhí)行默認(rèn)動(dòng)作,即不退出。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容