iOS研發(fā)助手DoraemonKit技術(shù)實(shí)現(xiàn)之Crash查看

一、前言

在日常開(kāi)發(fā)中或者測(cè)試過(guò)程中谴餐,我們的應(yīng)用可能會(huì)出現(xiàn)Crash的問(wèn)題姻政。對(duì)于這類問(wèn)題我們要抱著零容忍的態(tài)度,因?yàn)槿绻€上出現(xiàn)了這類問(wèn)題总寒,將會(huì)嚴(yán)重影響用戶的體驗(yàn)扶歪。

如果Crash出現(xiàn)的時(shí)候恰好是在開(kāi)發(fā)過(guò)程中,那么開(kāi)發(fā)者可以根據(jù)Xcode的調(diào)用堆椛阏ⅲ或者控制臺(tái)輸出的信息來(lái)定位問(wèn)題的原因善镰。但是,如果是在測(cè)試過(guò)程中的話就比較麻煩了年枕。常見(jiàn)的兩種解決方案是:

  1. 直接把測(cè)試手機(jī)拿來(lái)連接X(jué)code查看設(shè)備信息中的日志炫欺。
  2. 需要測(cè)試同學(xué)給出Crash的復(fù)現(xiàn)路徑,然后開(kāi)發(fā)者在調(diào)試過(guò)程中進(jìn)行復(fù)現(xiàn)熏兄。

不過(guò)品洛,以上兩種方式都不是很方便。那么問(wèn)題來(lái)了摩桶,有沒(méi)有更好的方式查看Crash日志桥状?答案當(dāng)然是肯定的。DoraemonKit的常用工具集中的Crash查看功能就解決了這個(gè)問(wèn)題硝清,可以直接在APP端查看Crash日志辅斟,下面我們來(lái)介紹下Crash查看功能的實(shí)現(xiàn)。

二芦拿、技術(shù)實(shí)現(xiàn)

在iOS的開(kāi)發(fā)過(guò)程中士飒,會(huì)出現(xiàn)各種各樣的Crash,那如何才能捕獲這些不同的Crash呢蔗崎?其實(shí)對(duì)于常見(jiàn)的Crash而言酵幕,可以分為兩類,一類是Objective-C異常缓苛,另一類是Mach異常芳撒,一些常見(jiàn)的異常如下圖所示:


常見(jiàn)異常

下面,我們就來(lái)看下這兩類異常應(yīng)當(dāng)如何捕獲未桥。

2.1 Objective-C異常

顧名思義番官,Objective-C異常就是指在OC層面(iOS庫(kù)、第三方庫(kù)出現(xiàn)錯(cuò)誤時(shí))出現(xiàn)的異常钢属。在介紹如何捕獲Objective-C異常之前我們先來(lái)看下常見(jiàn)的Objective-C異常包括哪些徘熔。

2.1.1 常見(jiàn)的Objective-C異常

一般來(lái)說(shuō),常見(jiàn)的Objective-C異常包括以下幾種:

  • NSInvalidArgumentException(非法參數(shù)異常)
    這類異常的主要原因是沒(méi)有對(duì)于參數(shù)的合法性進(jìn)行校驗(yàn)淆党,最常見(jiàn)的就是傳入nil作為參數(shù)酷师。例如讶凉,NSMutableDictionary添加key為nil的對(duì)象,測(cè)試代碼如下:
NSString *key = nil;
NSString *value = @"Hello";
NSMutableDictionary *mDic = [[NSMutableDictionary alloc] init];
[mDic setObject:value forKey:key];

運(yùn)行后控制臺(tái)輸出日志:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', 
reason: '*** -[__NSDictionaryM setObject:forKey:]: key cannot be nil'
  • NSRangeException(越界異常)
    這類異常的主要原因是沒(méi)有對(duì)于索引進(jìn)行合法性的檢查山孔,導(dǎo)致索引落在集合數(shù)據(jù)的合法范圍之外懂讯。例如,索引超出數(shù)組的范圍從而導(dǎo)致數(shù)組越界的問(wèn)題台颠,測(cè)試代碼如下:
    NSArray *array = @[@0, @1, @2];
    NSUInteger index = 3;
    NSNumber *value = [array objectAtIndex:index];

運(yùn)行后控制臺(tái)輸出日志:

*** Terminating app due to uncaught exception 'NSRangeException', 
reason: '*** -[__NSArrayI objectAtIndex:]: index 3 beyond bounds [0 .. 2]'
  • NSGenericException(通用異常)
    這類異常最容易出現(xiàn)在foreach操作中褐望,主要原因是在遍歷過(guò)程中進(jìn)行了元素的修改。例如串前,在for in循環(huán)中如果修改所遍歷的數(shù)組則會(huì)導(dǎo)致該問(wèn)題瘫里,測(cè)試代碼如下:
    NSMutableArray *mArray = [NSMutableArray arrayWithArray:@[@0, @1, @2]];
    for (NSNumber *num in mArray) {
        [mArray addObject:@3];
    }

運(yùn)行后控制臺(tái)輸出日志:

*** Terminating app due to uncaught exception 'NSGenericException', 
reason: '*** Collection <__NSArrayM: 0x600000c08660> was mutated while being enumerated.'
  • NSMallocException(內(nèi)存分配異常)
    這類異常的主要原因是無(wú)法分配足夠的內(nèi)存空間。例如荡碾,分配一塊超大的內(nèi)存空間就會(huì)導(dǎo)致此類的異常谨读,測(cè)試代碼如下:
    NSMutableData *mData = [[NSMutableData alloc] initWithCapacity:1];
    NSUInteger len = 1844674407370955161;
    [mData increaseLengthBy:len];

運(yùn)行后控制臺(tái)輸出日志:

*** Terminating app due to uncaught exception 'NSMallocException', 
reason: 'Failed to grow buffer'
  • NSFileHandleOperationException(文件處理異常)
    這類異常的主要原因是對(duì)文件進(jìn)行相關(guān)操作時(shí)產(chǎn)生了異常,如手機(jī)沒(méi)有足夠的存儲(chǔ)空間坛吁,文件讀寫權(quán)限問(wèn)題等劳殖。例如,對(duì)于一個(gè)只有讀權(quán)限的文件進(jìn)行寫操作拨脉,測(cè)試代碼如下:
    NSString *cacheDir = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
    NSString *filePath = [cacheDir stringByAppendingPathComponent:@"1.txt"];
    if (![[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
        NSString *str1 = @"Hello1";
        NSData *data1 = [str1 dataUsingEncoding:NSUTF8StringEncoding];
        [[NSFileManager defaultManager] createFileAtPath:filePath contents:data1 attributes:nil];
    }
    
    NSFileHandle *fileHandle = [NSFileHandle fileHandleForReadingAtPath:filePath];
    [fileHandle seekToEndOfFile];
    NSString *str2 = @"Hello2";
    NSData *data2 = [str2 dataUsingEncoding:NSUTF8StringEncoding];
    [fileHandle writeData:data2];
    [fileHandle closeFile];

運(yùn)行后控制臺(tái)輸出日志:

*** Terminating app due to uncaught exception 'NSFileHandleOperationException', 
reason: '*** -[NSConcreteFileHandle writeData:]: Bad file descriptor'

以上介紹了幾個(gè)常見(jiàn)的Objective-C異常哆姻,接下來(lái)我們來(lái)看下如何捕獲Objective-C異常。

2.1.2 捕獲Objective-C異常

如果是在開(kāi)發(fā)過(guò)程中玫膀,Objective-C異常導(dǎo)致的Crash會(huì)在Xcode的控制臺(tái)輸出異常的類型矛缨、原因以及調(diào)用堆棧,根據(jù)這些信息我們能夠迅速定位異常的原因并進(jìn)行修復(fù)匆骗。

那如果不是在開(kāi)發(fā)過(guò)程中,我們應(yīng)當(dāng)如何捕獲這些異常的信息呢誉简?

其實(shí)Apple已經(jīng)給我們提供了捕獲Objective-C異常的API碉就,就是NSSetUncaughtExceptionHandler。我們先來(lái)看下官方文檔是怎么描述的:

Sets the top-level error-handling function where you can perform last-minute logging before the program terminates.

意思就是通過(guò)這個(gè)API設(shè)置了異常處理函數(shù)之后闷串,就可以在程序終止前的最后一刻進(jìn)行日志的記錄瓮钥。這個(gè)功能正是我們想要的,使用起來(lái)也比較簡(jiǎn)單烹吵,代碼如下:

+ (void)registerHandler {
    NSSetUncaughtExceptionHandler(&DoraemonUncaughtExceptionHandler);
}

這里的參數(shù)DoraemonUncaughtExceptionHandler就是異常處理函數(shù)碉熄,它的定義如下:

// 崩潰時(shí)的回調(diào)函數(shù)
static void DoraemonUncaughtExceptionHandler(NSException * exception) {
    // 異常的堆棧信息
    NSArray * stackArray = [exception callStackSymbols];
    // 出現(xiàn)異常的原因
    NSString * reason = [exception reason];
    // 異常名稱
    NSString * name = [exception name];
    
    NSString * exceptionInfo = [NSString stringWithFormat:@"========uncaughtException異常錯(cuò)誤報(bào)告========\nname:%@\nreason:\n%@\ncallStackSymbols:\n%@", name, reason, [stackArray componentsJoinedByString:@"\n"]];
    
    // 保存崩潰日志到沙盒cache目錄
    [DoraemonCrashTool saveCrashLog:exceptionInfo fileName:@"Crash(Uncaught)"];
}

通過(guò)上面的代碼我們可以看到,在異常發(fā)生的時(shí)候肋拔,異常名稱锈津、出現(xiàn)異常的原因以及異常的堆棧信息都可以拿到。拿到這些信息之后凉蜂,保存到沙盒的cache目錄琼梆,然后就可以直接查看了性誉。

這里需要注意的是:對(duì)于一個(gè)APP來(lái)說(shuō),可能會(huì)集成多個(gè)Crash收集工具茎杂,如果大家都調(diào)用了NSSetUncaughtExceptionHandler來(lái)注冊(cè)異常處理函數(shù)错览,那么后注冊(cè)的將會(huì)覆蓋掉前面注冊(cè)的,導(dǎo)致前面注冊(cè)的異常處理函數(shù)不能正常工作煌往。

那應(yīng)當(dāng)如何解決這種覆蓋的問(wèn)題呢倾哺?其實(shí)思路很簡(jiǎn)單,在我們調(diào)用NSSetUncaughtExceptionHandler注冊(cè)異常處理函數(shù)之前刽脖,先拿到已有的異常處理函數(shù)并保存下來(lái)羞海。然后在我們的處理函數(shù)執(zhí)行之后,再調(diào)用之前保存的處理函數(shù)就可以了曾棕。這樣扣猫,后面注冊(cè)的就不會(huì)對(duì)之前注冊(cè)的產(chǎn)生影響了。

思路有了翘地,該如何實(shí)現(xiàn)呢申尤?通過(guò)Apple的文檔可以知道,有一個(gè)獲取之前異常處理函數(shù)的API衙耕,就是NSGetUncaughtExceptionHandler昧穿,通過(guò)它我們就可以獲取之前的異常處理函數(shù)了,代碼如下:

// 記錄之前的崩潰回調(diào)函數(shù)
static NSUncaughtExceptionHandler *previousUncaughtExceptionHandler = NULL;

+ (void)registerHandler {
    // Backup original handler
    previousUncaughtExceptionHandler = NSGetUncaughtExceptionHandler();
    
    NSSetUncaughtExceptionHandler(&DoraemonUncaughtExceptionHandler);
}

在我們?cè)O(shè)置自己的異常處理函數(shù)之前橙喘,先保存已有的異常處理函數(shù)时鸵。在處理異常的時(shí)候,我們自己的異常處理函數(shù)處理完畢之后厅瞎,需要將異常拋給之前保存的異常處理函數(shù)饰潜,代碼如下:

// 崩潰時(shí)的回調(diào)函數(shù)
static void DoraemonUncaughtExceptionHandler(NSException * exception) {
    // 異常的堆棧信息
    NSArray * stackArray = [exception callStackSymbols];
    // 出現(xiàn)異常的原因
    NSString * reason = [exception reason];
    // 異常名稱
    NSString * name = [exception name];
    
    NSString * exceptionInfo = [NSString stringWithFormat:@"========uncaughtException異常錯(cuò)誤報(bào)告========\nname:%@\nreason:\n%@\ncallStackSymbols:\n%@", name, reason, [stackArray componentsJoinedByString:@"\n"]];
    
    // 保存崩潰日志到沙盒cache目錄
    [DoraemonCrashTool saveCrashLog:exceptionInfo fileName:@"Crash(Uncaught)"];
    
    // 調(diào)用之前崩潰的回調(diào)函數(shù)
    if (previousUncaughtExceptionHandler) {
        previousUncaughtExceptionHandler(exception);
    }
}

到這里,就基本完成對(duì)于Objective-C異常的捕獲了和簸。

2.2 Mach異常

上一節(jié)介紹了Objective-C異常彭雾,本節(jié)來(lái)介紹下Mach異常,那究竟什么是Mach異常呢锁保?在回答這個(gè)問(wèn)題之前薯酝,我們先來(lái)看下一些相關(guān)的知識(shí)。

2.2.1 Mach相關(guān)概念

osx_architecture-kernels_drivers

上圖來(lái)自于Apple的Mac Technology Overview爽柒,對(duì)于Kernel and Device Drivers 這一層而言吴菠,OS X與iOS架構(gòu)大體上是一致的。其中浩村,內(nèi)核部分都是XNU做葵,而Mach就是XNU的微內(nèi)核核心。

Mach的職責(zé)主要是進(jìn)程和線程抽象心墅、虛擬內(nèi)存管理蜂挪、任務(wù)調(diào)度重挑、進(jìn)程間通信和消息傳遞機(jī)制等。

Mach微內(nèi)核中有幾個(gè)基本的概念:

  • task:擁有一組系統(tǒng)資源的對(duì)象棠涮,允許thread在其中執(zhí)行谬哀。
  • thread:執(zhí)行的基本單位,擁有task的上下文严肪,并共享其資源史煎。
  • port:task之間通訊的一組受保護(hù)的消息隊(duì)列,task可對(duì)任何port發(fā)送/接收數(shù)據(jù)驳糯。
  • message:有類型的數(shù)據(jù)對(duì)象集合篇梭,只可以發(fā)送到port。

BSD層則在Mach之上酝枢,提供一套可靠且更現(xiàn)代的API恬偷,提供了POSIX兼容性。

2.2.2 Mach異常與Unix信號(hào)

在了解到Mach一些相關(guān)概念之后帘睦,我們來(lái)看下什么是Mach異常袍患?這里引用《漫談iOS Crash收集框架》中對(duì)于Mach異常的解釋。

iOS系統(tǒng)自帶的 Apple’s Crash Reporter 記錄在設(shè)備中的Crash日志竣付,Exception Type項(xiàng)通常會(huì)包含兩個(gè)元素:Mach異常和Unix信號(hào)诡延。

Mach異常:允許在進(jìn)程里或進(jìn)程外處理,處理程序通過(guò)Mach RPC調(diào)用古胆。
Unix信號(hào):只在進(jìn)程中處理肆良,處理程序總是在發(fā)生錯(cuò)誤的線程上調(diào)用。

Mach異常是指最底層的內(nèi)核級(jí)異常逸绎,被定義在 <mach/exception_types.h>下 惹恃。每個(gè)thread,task棺牧,host都有一個(gè)異常端口數(shù)組巫糙,Mach的部分API暴露給了用戶態(tài),用戶態(tài)的開(kāi)發(fā)者可以直接通過(guò)Mach API設(shè)置thread陨帆,task曲秉,host的異常端口采蚀,來(lái)捕獲Mach異常疲牵,抓取Crash事件。

所有Mach異常都在host層被ux_exception轉(zhuǎn)換為相應(yīng)的Unix信號(hào)榆鼠,并通過(guò)threadsignal將信號(hào)投遞到出錯(cuò)的線程纲爸。iOS中的 POSIX API 就是通過(guò) Mach 之上的 BSD 層實(shí)現(xiàn)的。如下圖所示:

例如妆够,Exception Type:EXC_BAD_ACCESS (SIGSEGV)表示的意思是:Mach層的EXC_BAD_ACCESS異常识啦,在host層被轉(zhuǎn)換成SIGSEGV信號(hào)投遞到出錯(cuò)的線程负蚊。下圖展示了從Mach異常轉(zhuǎn)換成Unix信號(hào)的過(guò)程:

Mach異常轉(zhuǎn)換成Unix信號(hào)的過(guò)程

既然最終以信號(hào)的方式投遞到出錯(cuò)的線程,那么就可以通過(guò)注冊(cè)signalHandler來(lái)捕獲信號(hào):

signal(SIGSEGV,signalHandler);

捕獲Mach異惩窍或者Unix信號(hào)都可以抓到Crash事件家妆,這里我們使用了Unix信號(hào)方式進(jìn)行捕獲,主要原因如下:

  1. Mach異常沒(méi)有比較便利的捕獲方式冕茅,既然它最終會(huì)轉(zhuǎn)化成信號(hào)伤极,我們也可以通過(guò)捕獲信號(hào)來(lái)捕獲Crash事件。
  2. 轉(zhuǎn)換Unix信號(hào)是為了兼容更為流行的POSIX標(biāo)準(zhǔn)(SUS規(guī)范)姨伤,這樣不必了解Mach內(nèi)核也可以通過(guò)Unix信號(hào)的方式來(lái)兼容開(kāi)發(fā)哨坪。

基于以上原因,我們選擇了基于Unix信號(hào)的方式來(lái)捕獲異常乍楚。

2.2.3 信號(hào)釋義

Unix信號(hào)有很多種当编,詳細(xì)的定義可以在<sys/signal.h>中找到。下面列舉我們所監(jiān)控的常用信號(hào)以及它們的含義:

  • SIGABRT:調(diào)用abort函數(shù)生成的信號(hào)徒溪。
  • SIGBUS:非法地址忿偷,包括內(nèi)存地址對(duì)齊(alignment)出錯(cuò)。比如訪問(wèn)一個(gè)四個(gè)字長(zhǎng)的整數(shù)词渤,但其地址不是4的倍數(shù)牵舱。它與SIGSEGV的區(qū)別在于后者是由于對(duì)合法存儲(chǔ)地址的非法訪問(wèn)觸發(fā)的(如訪問(wèn)不屬于自己存儲(chǔ)空間或只讀存儲(chǔ)空間)。
  • SIGFPE:在發(fā)生致命的算術(shù)運(yùn)算錯(cuò)誤時(shí)發(fā)出缺虐。不僅包括浮點(diǎn)運(yùn)算錯(cuò)誤芜壁,還包括溢出及除數(shù)為0等其它所有的算術(shù)的錯(cuò)誤。
  • SIGILL:執(zhí)行了非法指令高氮。通常是因?yàn)榭蓤?zhí)行文件本身出現(xiàn)錯(cuò)誤慧妄,或者試圖執(zhí)行數(shù)據(jù)段。堆棧溢出時(shí)也有可能產(chǎn)生這個(gè)信號(hào)剪芍。
  • SIGPIPE:管道破裂塞淹。這個(gè)信號(hào)通常在進(jìn)程間通信產(chǎn)生罪裹,比如采用FIFO(管道)通信的兩個(gè)進(jìn)程饱普,讀管道沒(méi)打開(kāi)或者意外終止就往管道寫,寫進(jìn)程會(huì)收到SIGPIPE信號(hào)状共。此外用Socket通信的兩個(gè)進(jìn)程套耕,寫進(jìn)程在寫Socket的時(shí)候,讀進(jìn)程已經(jīng)終止峡继。
  • SIGSEGV:試圖訪問(wèn)未分配給自己的內(nèi)存冯袍,或試圖往沒(méi)有寫權(quán)限的內(nèi)存地址寫數(shù)據(jù)。
  • SIGSYS:非法的系統(tǒng)調(diào)用。
  • SIGTRAP:由斷點(diǎn)指令或其它trap指令產(chǎn)生康愤,由debugger使用儡循。

更多信號(hào)的釋義可以參考《iOS異常捕獲》

2.2.4 捕獲Unix信號(hào)

類似上一節(jié)中捕獲Objective-C異常的思路征冷,先注冊(cè)一個(gè)異常處理函數(shù)择膝,用于對(duì)信號(hào)的監(jiān)控。代碼如下:

+ (void)signalRegister {
    DoraemonSignalRegister(SIGABRT);
    DoraemonSignalRegister(SIGBUS);
    DoraemonSignalRegister(SIGFPE);
    DoraemonSignalRegister(SIGILL);
    DoraemonSignalRegister(SIGPIPE);
    DoraemonSignalRegister(SIGSEGV);
    DoraemonSignalRegister(SIGSYS);
    DoraemonSignalRegister(SIGTRAP);
}

static void DoraemonSignalRegister(int signal) {
    // Register Signal
    struct sigaction action;
    action.sa_sigaction = DoraemonSignalHandler;
    action.sa_flags = SA_NODEFER | SA_SIGINFO;
    sigemptyset(&action.sa_mask);
    sigaction(signal, &action, 0);
}

這里的DoraemonSignalHandler就是監(jiān)控信號(hào)的異常處理函數(shù)检激,它的定義如下:

static void DoraemonSignalHandler(int signal, siginfo_t* info, void* context) {
    NSMutableString *mstr = [[NSMutableString alloc] init];
    [mstr appendString:@"Signal Exception:\n"];
    [mstr appendString:[NSString stringWithFormat:@"Signal %@ was raised.\n", signalName(signal)]];
    [mstr appendString:@"Call Stack:\n"];
    
    // 這里過(guò)濾掉第一行日志
    // 因?yàn)樽?cè)了信號(hào)崩潰回調(diào)方法调榄,系統(tǒng)會(huì)來(lái)調(diào)用,將記錄在調(diào)用堆棧上呵扛,因此此行日志需要過(guò)濾掉
    for (NSUInteger index = 1; index < NSThread.callStackSymbols.count; index++) {
        NSString *str = [NSThread.callStackSymbols objectAtIndex:index];
        [mstr appendString:[str stringByAppendingString:@"\n"]];
    }
    
    [mstr appendString:@"threadInfo:\n"];
    [mstr appendString:[[NSThread currentThread] description]];
    
    // 保存崩潰日志到沙盒cache目錄
    [DoraemonCrashTool saveCrashLog:[NSString stringWithString:mstr] fileName:@"Crash(Signal)"];
    
    DoraemonClearSignalRigister();
}

這里有一點(diǎn)需要注意的是每庆,過(guò)濾掉了第一行日志。這是因?yàn)樽?cè)了信號(hào)崩潰的回調(diào)方法今穿,系統(tǒng)會(huì)來(lái)調(diào)用缤灵,將記錄在調(diào)用堆棧上,因此為了避免困擾將此行日志過(guò)濾掉蓝晒。

通過(guò)上面的代碼我們可以看到腮出,在異常發(fā)生時(shí),信號(hào)名芝薇、調(diào)用堆棧胚嘲、線程信息等都可以拿到。拿到這些信息之后洛二,保存到沙盒的cache目錄馋劈,然后就可以直接查看了。

類似捕獲Objective-C異沉浪唬可能出現(xiàn)的問(wèn)題妓雾,在集成多個(gè)Crash收集工具時(shí),如果大家對(duì)于相同的信號(hào)都注冊(cè)了異常處理函數(shù)垒迂,那么后注冊(cè)的將會(huì)覆蓋掉前面注冊(cè)的械姻,導(dǎo)致前面注冊(cè)的異常處理函數(shù)不能正常工作。

參考捕獲Objective-C異常時(shí)處理覆蓋問(wèn)題的思路机断,我們也可以先將已有的異常處理函數(shù)進(jìn)行保存楷拳,然后在我們的異常處理函數(shù)執(zhí)行之后,再調(diào)用之前保存的異常處理函數(shù)就可以了吏奸。具體實(shí)現(xiàn)的代碼如下:

static SignalHandler previousABRTSignalHandler = NULL;
static SignalHandler previousBUSSignalHandler  = NULL;
static SignalHandler previousFPESignalHandler  = NULL;
static SignalHandler previousILLSignalHandler  = NULL;
static SignalHandler previousPIPESignalHandler = NULL;
static SignalHandler previousSEGVSignalHandler = NULL;
static SignalHandler previousSYSSignalHandler  = NULL;
static SignalHandler previousTRAPSignalHandler = NULL;


+ (void)backupOriginalHandler {
    struct sigaction old_action_abrt;
    sigaction(SIGABRT, NULL, &old_action_abrt);
    if (old_action_abrt.sa_sigaction) {
        previousABRTSignalHandler = old_action_abrt.sa_sigaction;
    }
    
    struct sigaction old_action_bus;
    sigaction(SIGBUS, NULL, &old_action_bus);
    if (old_action_bus.sa_sigaction) {
        previousBUSSignalHandler = old_action_bus.sa_sigaction;
    }
    
    struct sigaction old_action_fpe;
    sigaction(SIGFPE, NULL, &old_action_fpe);
    if (old_action_fpe.sa_sigaction) {
        previousFPESignalHandler = old_action_fpe.sa_sigaction;
    }
    
    struct sigaction old_action_ill;
    sigaction(SIGILL, NULL, &old_action_ill);
    if (old_action_ill.sa_sigaction) {
        previousILLSignalHandler = old_action_ill.sa_sigaction;
    }
    
    struct sigaction old_action_pipe;
    sigaction(SIGPIPE, NULL, &old_action_pipe);
    if (old_action_pipe.sa_sigaction) {
        previousPIPESignalHandler = old_action_pipe.sa_sigaction;
    }
    
    struct sigaction old_action_segv;
    sigaction(SIGSEGV, NULL, &old_action_segv);
    if (old_action_segv.sa_sigaction) {
        previousSEGVSignalHandler = old_action_segv.sa_sigaction;
    }
    
    struct sigaction old_action_sys;
    sigaction(SIGSYS, NULL, &old_action_sys);
    if (old_action_sys.sa_sigaction) {
        previousSYSSignalHandler = old_action_sys.sa_sigaction;
    }
    
    struct sigaction old_action_trap;
    sigaction(SIGTRAP, NULL, &old_action_trap);
    if (old_action_trap.sa_sigaction) {
        previousTRAPSignalHandler = old_action_trap.sa_sigaction;
    }
}

這里需要注意的一點(diǎn)是欢揖,對(duì)于我們監(jiān)聽(tīng)的信號(hào)都要保存之前的異常處理函數(shù)。

在處理異常的時(shí)候苦丁,我們自己的異常處理函數(shù)處理完畢之后浸颓,需要將異常拋給之前保存的異常處理函數(shù)物臂,代碼如下:


static void DoraemonSignalHandler(int signal, siginfo_t* info, void* context) {
    NSMutableString *mstr = [[NSMutableString alloc] init];
    [mstr appendString:@"Signal Exception:\n"];
    [mstr appendString:[NSString stringWithFormat:@"Signal %@ was raised.\n", signalName(signal)]];
    [mstr appendString:@"Call Stack:\n"];
   
    // 這里過(guò)濾掉第一行日志
    // 因?yàn)樽?cè)了信號(hào)崩潰回調(diào)方法旺拉,系統(tǒng)會(huì)來(lái)調(diào)用产上,將記錄在調(diào)用堆棧上,因此此行日志需要過(guò)濾掉
    for (NSUInteger index = 1; index < NSThread.callStackSymbols.count; index++) {
        NSString *str = [NSThread.callStackSymbols objectAtIndex:index];
        [mstr appendString:[str stringByAppendingString:@"\n"]];
    }
    
    [mstr appendString:@"threadInfo:\n"];
    [mstr appendString:[[NSThread currentThread] description]];
    
    // 保存崩潰日志到沙盒cache目錄
    [DoraemonCrashTool saveCrashLog:[NSString stringWithString:mstr] fileName:@"Crash(Signal)"];
    
    DoraemonClearSignalRigister();
    
    // 調(diào)用之前崩潰的回調(diào)函數(shù)
    previousSignalHandler(signal, info, context);
}

到這里蛾狗,就基本完成對(duì)于Unix信號(hào)的捕獲了晋涣。

2.3 小結(jié)

通過(guò)前面的介紹,相信大家對(duì)如何捕獲Crash有了一定的了解沉桌,下面引用《Mach異承蝗担》中的一張圖對(duì)之前的內(nèi)容做一個(gè)總結(jié),如下所示:

三留凭、 踩過(guò)的坑

上面兩節(jié)分別介紹了如何捕獲Objective-C異常和Mach異常佃扼,本節(jié)主要是總結(jié)一下實(shí)現(xiàn)的過(guò)程中,遇到的一些問(wèn)題蔼夜。

3.1 通過(guò)Unix信號(hào)捕獲Objective-C異常的問(wèn)題

可能大家會(huì)覺(jué)得既然Unix信號(hào)可以捕獲底層的Mach異常兼耀,那為什么不能捕獲Objective-C異常呢?其實(shí)是可以捕獲的求冷,只是對(duì)于這種應(yīng)用級(jí)的異常瘤运,你會(huì)發(fā)現(xiàn)調(diào)用堆棧里并沒(méi)有你的代碼,無(wú)法定位問(wèn)題匠题。例如拯坟,數(shù)組越界這種Objective-C異常的代碼如下:

    NSArray *array = @[@0, @1, @2];
    NSUInteger index = 3;
    NSNumber *value = [array objectAtIndex:index];

如果我們使用Unix信號(hào)進(jìn)行捕獲,得到的Crash日志如下:

Signal Exception:
Signal SIGABRT was raised.
Call Stack:
1   libsystem_platform.dylib            0x00000001a6df0a20 <redacted> + 56
2   libsystem_pthread.dylib             0x00000001a6df6070 <redacted> + 380
3   libsystem_c.dylib                   0x00000001a6cd2d78 abort + 140
4   libc++abi.dylib                     0x00000001a639cf78 __cxa_bad_cast + 0
5   libc++abi.dylib                     0x00000001a639d120 <redacted> + 0
6   libobjc.A.dylib                     0x00000001a63b5e48 <redacted> + 124
7   libc++abi.dylib                     0x00000001a63a90fc <redacted> + 16
8   libc++abi.dylib                     0x00000001a63a8cec __cxa_rethrow + 144
9   libobjc.A.dylib                     0x00000001a63b5c10 objc_exception_rethrow + 44
10  CoreFoundation                      0x00000001a716e238 CFRunLoopRunSpecific + 544
11  GraphicsServices                    0x00000001a93e5584 GSEventRunModal + 100
12  UIKitCore                           0x00000001d4269054 UIApplicationMain + 212
13  DoraemonKitDemo                     0x00000001024babf0 main + 124
14  libdyld.dylib                       0x00000001a6c2ebb4 <redacted> + 4
threadInfo:
<NSThread: 0x280f01400>{number = 1, name = main}

可以看到韭山,通過(guò)上述調(diào)用堆棧我們無(wú)法定位問(wèn)題郁季。因此戚丸,我們需要拿到導(dǎo)致Crash的NSException窖逗,從中獲取異常的名稱帚豪、原因和調(diào)用堆棧拱雏,這樣才能準(zhǔn)確定位問(wèn)題秕狰。

所以捧弃,在DoraemonKit中我們采用了NSSetUncaughtExceptionHandler對(duì)于Objective-C異常進(jìn)行捕獲叠洗。

3.2 兩種異常共存的問(wèn)題

由于我們既捕獲了Objective-C異常愧膀,又捕獲了Mach異常禁舷,那么當(dāng)發(fā)生Objective-C異常的時(shí)候就會(huì)出現(xiàn)兩份Crash日志彪杉。

一份是通過(guò)NSSetUncaughtExceptionHandler設(shè)置異常處理函數(shù)生成的日志,另一份是通過(guò)捕獲Unix信號(hào)產(chǎn)生的日志牵咙。這兩份日志中派近,通過(guò)Unix信號(hào)捕獲的日志是無(wú)法定位問(wèn)題的,因此我們只需要NSSetUncaughtExceptionHandler中異常處理函數(shù)生成的日志即可洁桌。

那該怎么做才能阻止生成捕獲Unix信號(hào)的日志呢渴丸?在DoraemonKit中采取的方式是在Objective-C異常捕獲到Crash之后,主動(dòng)調(diào)用exit(0)或者kill(getpid(), SIGKILL)等方式讓程序退出。

3.3 調(diào)試的問(wèn)題

在捕獲Objective-C異常時(shí)谱轨,使用Xcode進(jìn)行調(diào)試可以清晰地看到調(diào)用流程戒幔。先調(diào)用了導(dǎo)致Crash的測(cè)試代碼,然后進(jìn)入異常處理函數(shù)捕獲Crash日志土童。

但是诗茎,在調(diào)試Unix信號(hào)的捕獲時(shí)會(huì)發(fā)現(xiàn)沒(méi)有進(jìn)入異常處理函數(shù)。這是怎么回事呢献汗?難道是我們對(duì)于Unix信號(hào)的捕獲沒(méi)有生效么敢订?其實(shí)并不是這樣的。主要是由于Xcode調(diào)試器的優(yōu)先級(jí)會(huì)高于我們對(duì)于Unix信號(hào)的捕獲罢吃,系統(tǒng)拋出的信號(hào)被Xcode調(diào)試器給捕獲了楚午,就不會(huì)再往上拋給我們的異常處理函數(shù)了。

因此尿招,如果我們要調(diào)試Unix信號(hào)的捕獲時(shí)醒叁,不能直接在Xcode調(diào)試器里進(jìn)行調(diào)試,一般使用的調(diào)試方式是:

  1. 通過(guò)Xcode查看設(shè)備的Device Logs泊业,從中得到我們打印的日志把沼。
  2. 直接將Crash保存到沙盒中,然后進(jìn)行查看吁伺。

在DoraemonKit中饮睬,我們直接將Crash保存到沙盒的cache目錄中,然后進(jìn)行查看篮奄。

3.4 多個(gè)Crash收集工具共存的問(wèn)題

正如之前所述捆愁,在同一個(gè)APP中集成多個(gè)Crash收集工具可能會(huì)存在強(qiáng)行覆蓋的問(wèn)題,即后注冊(cè)的異常處理函數(shù)會(huì)覆蓋掉之前注冊(cè)的異常處理函數(shù)窟却。

為了使得DoraemonKit不影響其他Crash收集工具昼丑,這里在注冊(cè)異常處理函數(shù)之前會(huì)先保存之前已經(jīng)注冊(cè)的異常處理函數(shù)。然后在我們的處理函數(shù)執(zhí)行之后夸赫,再調(diào)用之前保存的處理函數(shù)菩帝。這樣,DoraemonKit就不會(huì)對(duì)之前注冊(cè)的Crash收集工具產(chǎn)生影響了茬腿。

3.5 一些特殊的Crash

即使捕獲Crash的過(guò)程沒(méi)有問(wèn)題呼奢,還是會(huì)存在一些捕獲不到的情況。例如切平,短時(shí)間內(nèi)內(nèi)存急劇上升握础,這個(gè)時(shí)候APP會(huì)被系統(tǒng)kill掉。但是悴品,此時(shí)的Unix信號(hào)是SIGKILL禀综,該信號(hào)是用來(lái)立即結(jié)束程序的運(yùn)行简烘,不能被阻塞、處理和忽略定枷。因此孤澎,無(wú)法對(duì)此信號(hào)進(jìn)行捕獲。
針對(duì)內(nèi)存泄露依鸥,推薦一款iOS內(nèi)存泄露檢測(cè)工具M(jìn)LeaksFinder:MLeaksFinder

還有一些Crash雖然可以收集,但是日志中沒(méi)有自己的代碼悼沈,定位十分困難贱迟。野指針正是如此,針對(duì)這種情況絮供,推薦參考《如何定位Obj-C野指針隨機(jī)Crash》系列文章:
《如何定位Obj-C野指針隨機(jī)Crash(一):先提高野指針Crash率》
《如何定位Obj-C野指針隨機(jī)Crash(二):讓非必現(xiàn)Crash變成必現(xiàn)》
《如何定位Obj-C野指針隨機(jī)Crash(三):如何讓Crash自報(bào)家門》

四衣吠、總結(jié)

寫這篇文章主要是為了能夠讓大家對(duì)于DoraemonKit中Crash查看工具有一個(gè)快速的了解。由于時(shí)間倉(cāng)促壤靶,個(gè)人水平有限缚俏,如有錯(cuò)誤之處歡迎大家批評(píng)指正。

目前的Crash查看只是實(shí)現(xiàn)了最基本的功能贮乳,后續(xù)還需要不斷完善忧换。大家如果有什么好的想法,或者發(fā)現(xiàn)我們的這個(gè)項(xiàng)目有bug向拆,歡迎大家去github上提Issues或者直接Pull requests亚茬,我們會(huì)第一時(shí)間處理,也可以加入我們的qq交流群進(jìn)行交流浓恳,也希望我們這個(gè)工具集合能在大家的一起努力下刹缝,做得更加完善。

如果大家覺(jué)得我們這個(gè)項(xiàng)目還可以的話颈将,點(diǎn)上一顆star吧梢夯。

DoraemonKit項(xiàng)目地址:https://github.com/didi/DoraemonKit

相關(guān)文章:
iOS研發(fā)助手DoraemonKit技術(shù)實(shí)現(xiàn)(一)
iOS研發(fā)助手DoraemonKit技術(shù)實(shí)現(xiàn)(二)

五、參考文獻(xiàn)

《漫談iOS Crash收集框架》
《iOS異常捕獲》
《iOS內(nèi)功篇:淺談Crash》
《iOS Mach異常和signal信號(hào)》
《淺談Mach Exceptions》
《iOS監(jiān)控編程之崩潰監(jiān)控》
《Mach異城缁》

六颂砸、交流群

QQ交流群
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市死姚,隨后出現(xiàn)的幾起案子沾凄,更是在濱河造成了極大的恐慌,老刑警劉巖知允,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件撒蟀,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡温鸽,警方通過(guò)查閱死者的電腦和手機(jī)保屯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門手负,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人姑尺,你說(shuō)我怎么就攤上這事竟终。” “怎么了切蟋?”我有些...
    開(kāi)封第一講書人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵统捶,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我柄粹,道長(zhǎng)喘鸟,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任驻右,我火速辦了婚禮什黑,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘堪夭。我一直安慰自己愕把,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布森爽。 她就那樣靜靜地躺著恨豁,像睡著了一般。 火紅的嫁衣襯著肌膚如雪爬迟。 梳的紋絲不亂的頭發(fā)上圣絮,一...
    開(kāi)封第一講書人閱讀 51,631評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音雕旨,去河邊找鬼扮匠。 笑死,一個(gè)胖子當(dāng)著我的面吹牛凡涩,可吹牛的內(nèi)容都是我干的棒搜。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼活箕,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼力麸!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起育韩,我...
    開(kāi)封第一講書人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤克蚂,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后筋讨,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體埃叭,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年悉罕,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了赤屋。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片立镶。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖类早,靈堂內(nèi)的尸體忽然破棺而出媚媒,到底是詐尸還是另有隱情,我是刑警寧澤涩僻,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布缭召,位于F島的核電站,受9級(jí)特大地震影響逆日,放射性物質(zhì)發(fā)生泄漏嵌巷。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一屏富、第九天 我趴在偏房一處隱蔽的房頂上張望晴竞。 院中可真熱鬧蛙卤,春花似錦狠半、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至行嗤,卻和暖如春已日,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背栅屏。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工飘千, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人栈雳。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓护奈,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親哥纫。 傳聞我的和親對(duì)象是個(gè)殘疾皇子霉旗,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 本文就捕獲iOS Crash、Crash日志組成蛀骇、Crash日志符號(hào)化厌秒、異常信息解讀、常見(jiàn)的Crash五部分介紹擅憔。...
    xukuangbo_閱讀 1,582評(píng)論 0 0
  • 轉(zhuǎn)載(漫談 iOS Crash 收集框架) 前言 很早以前就和念茜認(rèn)識(shí)鸵闪,念茜不但技術(shù)功底扎實(shí),而且長(zhǎng)得很漂亮暑诸,說(shuō)她...
    狂風(fēng)無(wú)跡閱讀 3,309評(píng)論 1 11
  • 來(lái)源:程序媛念茜的博客 Crash日志收集 為了能夠第一時(shí)間發(fā)現(xiàn)程序問(wèn)題岛马,應(yīng)用程序需要實(shí)現(xiàn)自己的崩潰日志收集服務(wù)棉姐,...
    幸福的魚閱讀 1,168評(píng)論 0 2
  • 以下為文章正文,如果覺(jué)得有用啦逆,歡迎給她打賞伞矩。 為了能夠第一時(shí)間發(fā)現(xiàn)程序問(wèn)題,應(yīng)用程序需要實(shí)現(xiàn)自己的崩潰日志收集服務(wù)...
    赤色追風(fēng)閱讀 2,554評(píng)論 1 11
  • 一直很喜歡簡(jiǎn)書的這種簡(jiǎn)潔的閱讀模式夏志,干凈乃坤,不帶那么多的干擾,專注于文章的字里行間沟蔑,返樸歸真的恬淡最適宜人湿诊。來(lái)到這里...
    吃到停不下來(lái)的小松鼠閱讀 202評(píng)論 0 2