一, 前言
在日常开发中或者测试过程中, 我们的应用可能会出现 Crash 的问题. 对于这类问题我们要抱着零容忍的态度, 因为如果线上出现了这类问题, 将会严重影响用户的体验.
如果 Crash 出现的时候恰好是在开发过程中, 那么开发者可以根据 Xcode 的调用堆栈或者控制台输出的信息来定位问题的原因. 但是, 如果是在测试过程中的话就比较麻烦了. 常见的两种解决方案是:
直接把测试手机拿来连接 Xcode 查看设备信息中的日志.
需要测试同学给出 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 相关概念
上图来自于 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 收集框架》 https://nianxi.net/ios/ios-crash-reporter.html 中对于 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 信号方式进行捕获, 主要原因如下:
Mach 异常没有比较便利的捕获方式, 既然它最终会转化成信号, 我们也可以通过捕获信号来捕获 Crash 事件.
转换 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 调试器里进行调试, 一般使用的调试方式是:
通过 Xcode 查看设备的 Device Logs, 从中得到我们打印的日志.
直接将 Crash 保存到沙盒中, 然后进行查看.
在 DoraemonKit 中, 我们直接将 Crash 保存到沙盒的 cache 目录中, 然后进行查看.
3.4 多个 Crash 收集工具共存的问题
正如之前所述, 在同一个 App 中集成多个 Crash 收集工具可能会存在强行覆盖的问题, 即后注册的异常处理函数会覆盖掉之前注册的异常处理函数.
为了使得 DoraemonKit 不影响其他 Crash 收集工具, 这里在注册异常处理函数之前会先保存之前已经注册的异常处理函数. 然后在我们的处理函数执行之后, 再调用之前保存的处理函数. 这样, DoraemonKit 就不会对之前注册的 Crash 收集工具产生影响了.
3.5 一些特殊的 Crash
即使捕获 Crash 的过程没有问题, 还是会存在一些捕获不到的情况. 例如, 短时间内内存急剧上升, 这个时候 App 会被系统 kill 掉. 但是, 此时的 Unix 信号是 SIGKILL, 该信号是用来立即结束程序的运行, 不能被阻塞, 处理和忽略. 因此, 无法对此信号进行捕获. 针对内存泄露, 推荐一款 iOS 内存泄露检测工具 MLeaksFinder: https://github.com/Tencent/MLeaksFinder
还有一些 Crash 虽然可以收集, 但是日志中没有自己的代码, 定位十分困难. 野指针正是如此, 针对这种情况, 推荐参考《如何定位 Obj-C 野指针随机 Crash》系列文章:
《如何定位 Obj-C 野指针随机 Crash(一): 先提高野指针 Crash 率》
《如何定位 Obj-C 野指针随机 Crash(二): 让非必现 Crash 变成必现》
《如何定位 Obj-C 野指针随机 Crash(三): 如何让 Crash 自报家门》
四, 总结
写这篇文章主要是为了能够让大家对于 DoraemonKit 中 Crash 查看工具有一个快速的了解. 由于时间仓促, 个人水平有限, 如有错误之处欢迎大家批评指正.
来源: https://juejin.im/post/5c47ea96f265da613a545443