在平常開發中或者測試過程當中,咱們的應用可能會出現Crash的問題。對於這類問題咱們要抱着零容忍的態度,由於若是線上出現了這類問題,將會嚴重影響用戶的體驗。html
若是Crash出現的時候剛好是在開發過程當中,那麼開發者能夠根據Xcode的調用堆棧或者控制檯輸出的信息來定位問題的緣由。可是,若是是在測試過程當中的話就比較麻煩了。常見的兩種解決方案是:ios
不過,以上兩種方式都不是很方便。那麼問題來了,有沒有更好的方式查看Crash日誌?答案固然是確定的。DoraemonKit的經常使用工具集中的Crash查看功能就解決了這個問題,能夠直接在APP端查看Crash日誌,下面咱們來介紹下Crash查看功能的實現。c++
在iOS的開發過程當中,會出現各類各樣的Crash,那如何才能捕獲這些不一樣的Crash呢?其實對於常見的Crash而言,能夠分爲兩類,一類是Objective-C異常,另外一類是Mach異常,一些常見的異常以下圖所示: git
下面,咱們就來看下這兩類異常應當如何捕獲。github
顧名思義,Objective-C異常就是指在OC層面(iOS庫、第三方庫出現錯誤時)出現的異常。在介紹如何捕獲Objective-C異常以前咱們先來看下常見的Objective-C異常包括哪些。編程
通常來講,常見的Objective-C異常包括如下幾種:數組
這類異常的主要緣由是沒有對於參數的合法性進行校驗,最多見的就是傳入nil做爲參數。例如,NSMutableDictionary添加key爲nil的對象,測試代碼以下:bash
NSString *key = nil;
NSString *value = @"Hello";
NSMutableDictionary *mDic = [[NSMutableDictionary alloc] init];
[mDic setObject:value forKey:key];
複製代碼
運行後控制檯輸出日誌:架構
*** Terminating app due to uncaught exception 'NSInvalidArgumentException',
reason: '*** -[__NSDictionaryM setObject:forKey:]: key cannot be nil'
複製代碼
這類異常的主要緣由是沒有對於索引進行合法性的檢查,致使索引落在集合數據的合法範圍以外。例如,索引超出數組的範圍從而致使數組越界的問題,測試代碼以下:app
NSArray *array = @[@0, @1, @2];
NSUInteger index = 3;
NSNumber *value = [array objectAtIndex:index];
複製代碼
運行後控制檯輸出日誌:
*** Terminating app due to uncaught exception 'NSRangeException',
reason: '*** -[__NSArrayI objectAtIndex:]: index 3 beyond bounds [0 .. 2]'
複製代碼
這類異常最容易出如今foreach操做中,主要緣由是在遍歷過程當中進行了元素的修改。例如,在for in循環中若是修改所遍歷的數組則會致使該問題,測試代碼以下:
NSMutableArray *mArray = [NSMutableArray arrayWithArray:@[@0, @1, @2]];
for (NSNumber *num in mArray) {
[mArray addObject:@3];
}
複製代碼
運行後控制檯輸出日誌:
*** Terminating app due to uncaught exception 'NSGenericException',
reason: '*** Collection <__NSArrayM: 0x600000c08660> was mutated while being enumerated.'
複製代碼
這類異常的主要緣由是沒法分配足夠的內存空間。例如,分配一塊超大的內存空間就會致使此類的異常,測試代碼以下:
NSMutableData *mData = [[NSMutableData alloc] initWithCapacity:1];
NSUInteger len = 1844674407370955161;
[mData increaseLengthBy:len];
複製代碼
運行後控制檯輸出日誌:
*** Terminating app due to uncaught exception 'NSMallocException',
reason: 'Failed to grow buffer'
複製代碼
這類異常的主要緣由是對文件進行相關操做時產生了異常,如手機沒有足夠的存儲空間,文件讀寫權限問題等。例如,對於一個只有讀權限的文件進行寫操做,測試代碼以下:
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];
複製代碼
運行後控制檯輸出日誌:
*** Terminating app due to uncaught exception 'NSFileHandleOperationException',
reason: '*** -[NSConcreteFileHandle writeData:]: Bad file descriptor'
複製代碼
以上介紹了幾個常見的Objective-C異常,接下來咱們來看下如何捕獲Objective-C異常。
若是是在開發過程當中,Objective-C異常致使的Crash會在Xcode的控制檯輸出異常的類型、緣由以及調用堆棧,根據這些信息咱們可以迅速定位異常的緣由並進行修復。
那若是不是在開發過程當中,咱們應當如何捕獲這些異常的信息呢?
其實Apple已經給咱們提供了捕獲Objective-C異常的API,就是NSSetUncaughtExceptionHandler
。咱們先來看下官方文檔是怎麼描述的:
Sets the top-level error-handling function where you can perform last-minute logging before the program terminates.
意思就是經過這個API設置了異常處理函數以後,就能夠在程序終止前的最後一刻進行日誌的記錄。這個功能正是咱們想要的,使用起來也比較簡單,代碼以下:
+ (void)registerHandler {
NSSetUncaughtExceptionHandler(&DoraemonUncaughtExceptionHandler);
}
複製代碼
這裏的參數DoraemonUncaughtExceptionHandler
就是異常處理函數,它的定義以下:
// 崩潰時的回調函數
static void DoraemonUncaughtExceptionHandler(NSException * exception) {
// 異常的堆棧信息
NSArray * stackArray = [exception callStackSymbols];
// 出現異常的緣由
NSString * reason = [exception reason];
// 異常名稱
NSString * name = [exception name];
NSString * exceptionInfo = [NSString stringWithFormat:@"========uncaughtException異常錯誤報告========\nname:%@\nreason:\n%@\ncallStackSymbols:\n%@", name, reason, [stackArray componentsJoinedByString:@"\n"]];
// 保存崩潰日誌到沙盒cache目錄
[DoraemonCrashTool saveCrashLog:exceptionInfo fileName:@"Crash(Uncaught)"];
}
複製代碼
經過上面的代碼咱們能夠看到,在異常發生的時候,異常名稱、出現異常的緣由以及異常的堆棧信息均可以拿到。拿到這些信息以後,保存到沙盒的cache目錄,而後就能夠直接查看了。
這裏須要注意的是:對於一個APP來講,可能會集成多個Crash收集工具,若是你們都調用了NSSetUncaughtExceptionHandler
來註冊異常處理函數,那麼後註冊的將會覆蓋掉前面註冊的,致使前面註冊的異常處理函數不能正常工做。
那應當如何解決這種覆蓋的問題呢?其實思路很簡單,在咱們調用NSSetUncaughtExceptionHandler
註冊異常處理函數以前,先拿到已有的異常處理函數並保存下來。而後在咱們的處理函數執行以後,再調用以前保存的處理函數就能夠了。這樣,後面註冊的就不會對以前註冊的產生影響了。
思路有了,該如何實現呢?經過Apple的文檔能夠知道,有一個獲取以前異常處理函數的API,就是NSGetUncaughtExceptionHandler
,經過它咱們就能夠獲取以前的異常處理函數了,代碼以下:
// 記錄以前的崩潰回調函數
static NSUncaughtExceptionHandler *previousUncaughtExceptionHandler = NULL;
+ (void)registerHandler {
// Backup original handler
previousUncaughtExceptionHandler = NSGetUncaughtExceptionHandler();
NSSetUncaughtExceptionHandler(&DoraemonUncaughtExceptionHandler);
}
複製代碼
在咱們設置本身的異常處理函數以前,先保存已有的異常處理函數。在處理異常的時候,咱們本身的異常處理函數處理完畢以後,須要將異常拋給以前保存的異常處理函數,代碼以下:
// 崩潰時的回調函數
static void DoraemonUncaughtExceptionHandler(NSException * exception) {
// 異常的堆棧信息
NSArray * stackArray = [exception callStackSymbols];
// 出現異常的緣由
NSString * reason = [exception reason];
// 異常名稱
NSString * name = [exception name];
NSString * exceptionInfo = [NSString stringWithFormat:@"========uncaughtException異常錯誤報告========\nname:%@\nreason:\n%@\ncallStackSymbols:\n%@", name, reason, [stackArray componentsJoinedByString:@"\n"]];
// 保存崩潰日誌到沙盒cache目錄
[DoraemonCrashTool saveCrashLog:exceptionInfo fileName:@"Crash(Uncaught)"];
// 調用以前崩潰的回調函數
if (previousUncaughtExceptionHandler) {
previousUncaughtExceptionHandler(exception);
}
}
複製代碼
到這裏,就基本完成對於Objective-C異常的捕獲了。
上一節介紹了Objective-C異常,本節來介紹下Mach異常,那究竟什麼是Mach異常呢?在回答這個問題以前,咱們先來看下一些相關的知識。
Mach的職責主要是進程和線程抽象、虛擬內存管理、任務調度、進程間通訊和消息傳遞機制等。
Mach微內核中有幾個基本的概念:
BSD層則在Mach之上,提供一套可靠且更現代的API,提供了POSIX兼容性。
在瞭解到Mach一些相關概念以後,咱們來看下什麼是Mach異常?這裏引用《漫談iOS Crash收集框架》中對於Mach異常的解釋。
iOS系統自帶的 Apple’s Crash Reporter 記錄在設備中的Crash日誌,Exception Type項一般會包含兩個元素:Mach異常和Unix信號。
Mach異常:容許在進程裏或進程外處理,處理程序經過Mach RPC調用。 Unix信號:只在進程中處理,處理程序老是在發生錯誤的線程上調用。
Mach異常是指最底層的內核級異常,被定義在 <mach/exception_types.h>
下 。每一個thread,task,host都有一個異常端口數組,Mach的部分API暴露給了用戶態,用戶態的開發者能夠直接經過Mach API設置thread,task,host的異常端口,來捕獲Mach異常,抓取Crash事件。
全部Mach異常都在host層被ux_exception
轉換爲相應的Unix信號,並經過threadsignal
將信號投遞到出錯的線程。iOS中的 POSIX API 就是經過 Mach 之上的 BSD 層實現的。以下圖所示:
例如,Exception Type:EXC_BAD_ACCESS (SIGSEGV)
表示的意思是:Mach層的EXC_BAD_ACCESS
異常,在host層被轉換成SIGSEGV
信號投遞到出錯的線程。下圖展現了從Mach異常轉換成Unix信號的過程:
既然最終以信號的方式投遞到出錯的線程,那麼就能夠經過註冊signalHandler來捕獲信號:
signal(SIGSEGV,signalHandler);
複製代碼
捕獲Mach異常或者Unix信號均可以抓到Crash事件,這裏咱們使用了Unix信號方式進行捕獲,主要緣由以下:
基於以上緣由,咱們選擇了基於Unix信號的方式來捕獲異常。
Unix信號有不少種,詳細的定義能夠在<sys/signal.h>
中找到。下面列舉咱們所監控的經常使用信號以及它們的含義:
更多信號的釋義能夠參考《iOS異常捕獲》。
相似上一節中捕獲Objective-C異常的思路,先註冊一個異常處理函數,用於對信號的監控。代碼以下:
+ (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
就是監控信號的異常處理函數,它的定義以下:
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"];
// 這裏過濾掉第一行日誌
// 由於註冊了信號崩潰回調方法,系統會來調用,將記錄在調用堆棧上,所以此行日誌須要過濾掉
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();
}
複製代碼
這裏有一點須要注意的是,過濾掉了第一行日誌。這是由於註冊了信號崩潰的回調方法,系統會來調用,將記錄在調用堆棧上,所以爲了不困擾將此行日誌過濾掉。
經過上面的代碼咱們能夠看到,在異常發生時,信號名、調用堆棧、線程信息等均可以拿到。拿到這些信息以後,保存到沙盒的cache目錄,而後就能夠直接查看了。
相似捕獲Objective-C異常可能出現的問題,在集成多個Crash收集工具時,若是你們對於相同的信號都註冊了異常處理函數,那麼後註冊的將會覆蓋掉前面註冊的,致使前面註冊的異常處理函數不能正常工做。
參考捕獲Objective-C異常時處理覆蓋問題的思路,咱們也能夠先將已有的異常處理函數進行保存,而後在咱們的異常處理函數執行以後,再調用以前保存的異常處理函數就能夠了。具體實現的代碼以下:
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;
}
}
複製代碼
這裏須要注意的一點是,對於咱們監聽的信號都要保存以前的異常處理函數。
在處理異常的時候,咱們本身的異常處理函數處理完畢以後,須要將異常拋給以前保存的異常處理函數,代碼以下:
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"];
// 這裏過濾掉第一行日誌
// 由於註冊了信號崩潰回調方法,系統會來調用,將記錄在調用堆棧上,所以此行日誌須要過濾掉
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();
// 調用以前崩潰的回調函數
previousSignalHandler(signal, info, context);
}
複製代碼
到這裏,就基本完成對於Unix信號的捕獲了。
經過前面的介紹,相信你們對如何捕獲Crash有了必定的瞭解,下面引用《Mach異常》中的一張圖對以前的內容作一個總結,以下所示:
上面兩節分別介紹瞭如何捕獲Objective-C異常和Mach異常,本節主要是總結一下實現的過程當中,遇到的一些問題。
可能你們會以爲既然Unix信號能夠捕獲底層的Mach異常,那爲何不能捕獲Objective-C異常呢?實際上是能夠捕獲的,只是對於這種應用級的異常,你會發現調用堆棧裏並無你的代碼,沒法定位問題。例如,數組越界這種Objective-C異常的代碼以下:
NSArray *array = @[@0, @1, @2];
NSUInteger index = 3;
NSNumber *value = [array objectAtIndex:index];
複製代碼
若是咱們使用Unix信號進行捕獲,獲得的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}
複製代碼
能夠看到,經過上述調用堆棧咱們沒法定位問題。所以,咱們須要拿到致使Crash的NSException,從中獲取異常的名稱、緣由和調用堆棧,這樣才能準肯定位問題。
因此,在DoraemonKit中咱們採用了NSSetUncaughtExceptionHandler
對於Objective-C異常進行捕獲。
因爲咱們既捕獲了Objective-C異常,又捕獲了Mach異常,那麼當發生Objective-C異常的時候就會出現兩份Crash日誌。
一份是經過NSSetUncaughtExceptionHandler
設置異常處理函數生成的日誌,另外一份是經過捕獲Unix信號產生的日誌。這兩份日誌中,經過Unix信號捕獲的日誌是沒法定位問題的,所以咱們只須要NSSetUncaughtExceptionHandler
中異常處理函數生成的日誌便可。
那該怎麼作才能阻止生成捕獲Unix信號的日誌呢?在DoraemonKit中採起的方式是在Objective-C異常捕獲到Crash以後,主動調用exit(0)
或者kill(getpid(), SIGKILL)
等方式讓程序退出。
在捕獲Objective-C異常時,使用Xcode進行調試能夠清晰地看到調用流程。先調用了致使Crash的測試代碼,而後進入異常處理函數捕獲Crash日誌。
可是,在調試Unix信號的捕獲時會發現沒有進入異常處理函數。這是怎麼回事呢?難道是咱們對於Unix信號的捕獲沒有生效麼?其實並非這樣的。主要是因爲Xcode調試器的優先級會高於咱們對於Unix信號的捕獲,系統拋出的信號被Xcode調試器給捕獲了,就不會再往上拋給咱們的異常處理函數了。
所以,若是咱們要調試Unix信號的捕獲時,不能直接在Xcode調試器裏進行調試,通常使用的調試方式是:
在DoraemonKit中,咱們直接將Crash保存到沙盒的cache目錄中,而後進行查看。
正如以前所述,在同一個APP中集成多個Crash收集工具可能會存在強行覆蓋的問題,即後註冊的異常處理函數會覆蓋掉以前註冊的異常處理函數。
爲了使得DoraemonKit不影響其餘Crash收集工具,這裏在註冊異常處理函數以前會先保存以前已經註冊的異常處理函數。而後在咱們的處理函數執行以後,再調用以前保存的處理函數。這樣,DoraemonKit就不會對以前註冊的Crash收集工具產生影響了。
即便捕獲Crash的過程沒有問題,仍是會存在一些捕獲不到的狀況。例如,短期內內存急劇上升,這個時候APP會被系統kill掉。可是,此時的Unix信號是SIGKILL,該信號是用來當即結束程序的運行,不能被阻塞、處理和忽略。所以,沒法對此信號進行捕獲。 針對內存泄露,推薦一款iOS內存泄露檢測工具MLeaksFinder:MLeaksFinder
還有一些Crash雖然能夠收集,可是日誌中沒有本身的代碼,定位十分困難。野指針正是如此,針對這種狀況,推薦參考《如何定位Obj-C野指針隨機Crash》系列文章:
《如何定位Obj-C野指針隨機Crash(一):先提升野指針Crash率》
《如何定位Obj-C野指針隨機Crash(二):讓非必現Crash變成必現》
《如何定位Obj-C野指針隨機Crash(三):如何讓Crash自報家門》
寫這篇文章主要是爲了可以讓你們對於DoraemonKit中Crash查看工具備一個快速的瞭解。因爲時間倉促,我的水平有限,若有錯誤之處歡迎你們批評指正。
目前的Crash查看只是實現了最基本的功能,後續還須要不斷完善。你們若是有什麼好的想法,或者發現咱們的這個項目有bug,歡迎你們去github上提Issues或者直接Pull requests,咱們會第一時間處理,也能夠加入咱們的qq交流羣進行交流,也但願咱們這個工具集合能在你們的一塊兒努力下,作得更加完善。
若是你們以爲咱們這個項目還能夠的話,點上一顆star吧。
DoraemonKit項目地址:github.com/didi/Doraem…
相關文章: