iOS研發助手DoraemonKit技術實現之Crash查看

一、前言

在日常開發中或者測試過程中,我們的應用可能會出現Crash的問題。對于這類問題我們要抱著零容忍的態度,因為如果線上出現了這類問題,將會嚴重影響用戶的體驗。

如果Crash出現的時候恰好是在開發過程中,那么開發者可以根據Xcode的調用堆棧或者控制臺輸出的信息來定位問題的原因。但是,如果是在測試過程中的話就比較麻煩了。常見的兩種解決方案是:

  1. 直接把測試手機拿來連接Xcode查看設備信息中的日志。
  2. 需要測試同學給出Crash的復現路徑,然后開發者在調試過程中進行復現。

不過,以上兩種方式都不是很方便。那么問題來了,有沒有更好的方式查看Crash日志?答案當然是肯定的。DoraemonKit的常用工具集中的Crash查看功能就解決了這個問題,可以直接在APP端查看Crash日志,下面我們來介紹下Crash查看功能的實現。

二、技術實現

在iOS的開發過程中,會出現各種各樣的Crash,那如何才能捕獲這些不同的Crash呢?其實對于常見的Crash而言,可以分為兩類,一類是Objective-C異常,另一類是Mach異常,一些常見的異常如下圖所示:


常見異常

下面,我們就來看下這兩類異常應當如何捕獲。

2.1 Objective-C異常

顧名思義,Objective-C異常就是指在OC層面(iOS庫、第三方庫出現錯誤時)出現的異常。在介紹如何捕獲Objective-C異常之前我們先來看下常見的Objective-C異常包括哪些。

2.1.1 常見的Objective-C異常

一般來說,常見的Objective-C異常包括以下幾種:

  • NSInvalidArgumentException(非法參數異常)
    這類異常的主要原因是沒有對于參數的合法性進行校驗,最常見的就是傳入nil作為參數。例如,NSMutableDictionary添加key為nil的對象,測試代碼如下:
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'
  • NSRangeException(越界異常)
    這類異常的主要原因是沒有對于索引進行合法性的檢查,導致索引落在集合數據的合法范圍之外。例如,索引超出數組的范圍從而導致數組越界的問題,測試代碼如下:
    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]'
  • NSGenericException(通用異常)
    這類異常最容易出現在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.'
  • NSMallocException(內存分配異常)
    這類異常的主要原因是無法分配足夠的內存空間。例如,分配一塊超大的內存空間就會導致此類的異常,測試代碼如下:
    NSMutableData *mData = [[NSMutableData alloc] initWithCapacity:1];
    NSUInteger len = 1844674407370955161;
    [mData increaseLengthBy:len];

運行后控制臺輸出日志:

*** Terminating app due to uncaught exception 'NSMallocException', 
reason: 'Failed to grow buffer'
  • NSFileHandleOperationException(文件處理異常)
    這類異常的主要原因是對文件進行相關操作時產生了異常,如手機沒有足夠的存儲空間,文件讀寫權限問題等。例如,對于一個只有讀權限的文件進行寫操作,測試代碼如下:
    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異常。

2.1.2 捕獲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異常的捕獲了。

2.2 Mach異常

上一節介紹了Objective-C異常,本節來介紹下Mach異常,那究竟什么是Mach異常呢?在回答這個問題之前,我們先來看下一些相關的知識。

2.2.1 Mach相關概念

osx_architecture-kernels_drivers

上圖來自于Apple的Mac Technology Overview,對于Kernel and Device Drivers 這一層而言,OS X與iOS架構大體上是一致的。其中,內核部分都是XNU,而Mach就是XNU的微內核核心。

Mach的職責主要是進程和線程抽象、虛擬內存管理、任務調度、進程間通信和消息傳遞機制等。

Mach微內核中有幾個基本的概念:

  • task:擁有一組系統資源的對象,允許thread在其中執行。
  • thread:執行的基本單位,擁有task的上下文,并共享其資源。
  • port:task之間通訊的一組受保護的消息隊列,task可對任何port發送/接收數據。
  • message:有類型的數據對象集合,只可以發送到port。

BSD層則在Mach之上,提供一套可靠且更現代的API,提供了POSIX兼容性。

2.2.2 Mach異常與Unix信號

在了解到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信號的過程:

Mach異常轉換成Unix信號的過程

既然最終以信號的方式投遞到出錯的線程,那么就可以通過注冊signalHandler來捕獲信號:

signal(SIGSEGV,signalHandler);

捕獲Mach異常或者Unix信號都可以抓到Crash事件,這里我們使用了Unix信號方式進行捕獲,主要原因如下:

  1. Mach異常沒有比較便利的捕獲方式,既然它最終會轉化成信號,我們也可以通過捕獲信號來捕獲Crash事件。
  2. 轉換Unix信號是為了兼容更為流行的POSIX標準(SUS規范),這樣不必了解Mach內核也可以通過Unix信號的方式來兼容開發。

基于以上原因,我們選擇了基于Unix信號的方式來捕獲異常。

2.2.3 信號釋義

Unix信號有很多種,詳細的定義可以在<sys/signal.h>中找到。下面列舉我們所監控的常用信號以及它們的含義:

  • SIGABRT:調用abort函數生成的信號。
  • SIGBUS:非法地址,包括內存地址對齊(alignment)出錯。比如訪問一個四個字長的整數,但其地址不是4的倍數。它與SIGSEGV的區別在于后者是由于對合法存儲地址的非法訪問觸發的(如訪問不屬于自己存儲空間或只讀存儲空間)。
  • SIGFPE:在發生致命的算術運算錯誤時發出。不僅包括浮點運算錯誤,還包括溢出及除數為0等其它所有的算術的錯誤。
  • SIGILL:執行了非法指令。通常是因為可執行文件本身出現錯誤,或者試圖執行數據段。堆棧溢出時也有可能產生這個信號。
  • SIGPIPE:管道破裂。這個信號通常在進程間通信產生,比如采用FIFO(管道)通信的兩個進程,讀管道沒打開或者意外終止就往管道寫,寫進程會收到SIGPIPE信號。此外用Socket通信的兩個進程,寫進程在寫Socket的時候,讀進程已經終止。
  • SIGSEGV:試圖訪問未分配給自己的內存,或試圖往沒有寫權限的內存地址寫數據。
  • SIGSYS:非法的系統調用。
  • SIGTRAP:由斷點指令或其它trap指令產生,由debugger使用。

更多信號的釋義可以參考《iOS異常捕獲》

2.2.4 捕獲Unix信號

類似上一節中捕獲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信號的捕獲了。

2.3 小結

通過前面的介紹,相信大家對如何捕獲Crash有了一定的了解,下面引用《Mach異常》中的一張圖對之前的內容做一個總結,如下所示:

三、 踩過的坑

上面兩節分別介紹了如何捕獲Objective-C異常和Mach異常,本節主要是總結一下實現的過程中,遇到的一些問題。

3.1 通過Unix信號捕獲Objective-C異常的問題

可能大家會覺得既然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異常進行捕獲。

3.2 兩種異常共存的問題

由于我們既捕獲了Objective-C異常,又捕獲了Mach異常,那么當發生Objective-C異常的時候就會出現兩份Crash日志。

一份是通過NSSetUncaughtExceptionHandler設置異常處理函數生成的日志,另一份是通過捕獲Unix信號產生的日志。這兩份日志中,通過Unix信號捕獲的日志是無法定位問題的,因此我們只需要NSSetUncaughtExceptionHandler中異常處理函數生成的日志即可。

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

3.3 調試的問題

在捕獲Objective-C異常時,使用Xcode進行調試可以清晰地看到調用流程。先調用了導致Crash的測試代碼,然后進入異常處理函數捕獲Crash日志。

但是,在調試Unix信號的捕獲時會發現沒有進入異常處理函數。這是怎么回事呢?難道是我們對于Unix信號的捕獲沒有生效么?其實并不是這樣的。主要是由于Xcode調試器的優先級會高于我們對于Unix信號的捕獲,系統拋出的信號被Xcode調試器給捕獲了,就不會再往上拋給我們的異常處理函數了。

因此,如果我們要調試Unix信號的捕獲時,不能直接在Xcode調試器里進行調試,一般使用的調試方式是:

  1. 通過Xcode查看設備的Device Logs,從中得到我們打印的日志。
  2. 直接將Crash保存到沙盒中,然后進行查看。

在DoraemonKit中,我們直接將Crash保存到沙盒的cache目錄中,然后進行查看。

3.4 多個Crash收集工具共存的問題

正如之前所述,在同一個APP中集成多個Crash收集工具可能會存在強行覆蓋的問題,即后注冊的異常處理函數會覆蓋掉之前注冊的異常處理函數。

為了使得DoraemonKit不影響其他Crash收集工具,這里在注冊異常處理函數之前會先保存之前已經注冊的異常處理函數。然后在我們的處理函數執行之后,再調用之前保存的處理函數。這樣,DoraemonKit就不會對之前注冊的Crash收集工具產生影響了。

3.5 一些特殊的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項目地址:https://github.com/didi/DoraemonKit

相關文章:
iOS研發助手DoraemonKit技術實現(一)
iOS研發助手DoraemonKit技術實現(二)

五、參考文獻

《漫談iOS Crash收集框架》
《iOS異常捕獲》
《iOS內功篇:淺談Crash》
《iOS Mach異常和signal信號》
《淺談Mach Exceptions》
《iOS監控編程之崩潰監控》
《Mach異常》

六、交流群

QQ交流群
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 本文就捕獲iOS Crash、Crash日志組成、Crash日志符號化、異常信息解讀、常見的Crash五部分介紹。...
    xukuangbo_閱讀 1,593評論 0 0
  • 轉載(漫談 iOS Crash 收集框架) 前言 很早以前就和念茜認識,念茜不但技術功底扎實,而且長得很漂亮,說她...
    狂風無跡閱讀 3,372評論 1 11
  • 來源:程序媛念茜的博客 Crash日志收集 為了能夠第一時間發現程序問題,應用程序需要實現自己的崩潰日志收集服務,...
    幸福的魚閱讀 1,179評論 0 2
  • 以下為文章正文,如果覺得有用,歡迎給她打賞。 為了能夠第一時間發現程序問題,應用程序需要實現自己的崩潰日志收集服務...
    赤色追風閱讀 2,566評論 1 11
  • 一直很喜歡簡書的這種簡潔的閱讀模式,干凈,不帶那么多的干擾,專注于文章的字里行間,返樸歸真的恬淡最適宜人。來到這里...
    吃到停不下來的小松鼠閱讀 207評論 0 2