在平常开发中或者测试过程当中,咱们的应用可能会出现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异常包括如下几种:数组
NSString *key = nil;
NSString *value = @"Hello";
NSMutableDictionary *mDic = [[NSMutableDictionary alloc] init];
[mDic setObject:value forKey:key];
复制代码
运行后控制台输出日志:bash
*** Terminating app due to uncaught exception 'NSInvalidArgumentException',
reason: '*** -[__NSDictionaryM setObject:forKey:]: key cannot be nil'
复制代码
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]'
复制代码
NSMutableArray *mArray = [NSMutableArray arrayWithArray:@[@0, @1, @2]];
for (NSNumber *num in mArray) {
[mArray addObject:@3];
}
复制代码
运行后控制台输出日志:app
*** 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…
DoraemonKit项目截图: