一、前言
在日常開(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)的兩種解決方案是:
- 直接把測(cè)試手機(jī)拿來(lái)連接X(jué)code查看設(shè)備信息中的日志炫欺。
- 需要測(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)的異常如下圖所示:
下面,我們就來(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)概念
上圖來(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ò)程:
既然最終以信號(hào)的方式投遞到出錯(cuò)的線程,那么就可以通過(guò)注冊(cè)signalHandler來(lái)捕獲信號(hào):
signal(SIGSEGV,signalHandler);
捕獲Mach異惩窍或者Unix信號(hào)都可以抓到Crash事件家妆,這里我們使用了Unix信號(hào)方式進(jìn)行捕獲,主要原因如下:
- Mach異常沒(méi)有比較便利的捕獲方式冕茅,既然它最終會(huì)轉(zhuǎn)化成信號(hào)伤极,我們也可以通過(guò)捕獲信號(hào)來(lái)捕獲Crash事件。
- 轉(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)試方式是:
- 通過(guò)Xcode查看設(shè)備的Device Logs泊业,從中得到我們打印的日志把沼。
- 直接將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異城缁》