1. 异常的类型
Mach异常
:是指最底层的内核级异常。用户态?
的开发者可以直接通过Mach API
设置thread,task,host?
的异常端口,来捕获Mach异常Unix信号
:又称BSD?
信号,如果开发者没有捕获Mach
异常,则会被host
层的方法ux_exception()
将异常转换为对应的UNIX
信号,并通过方法threadsignal()
将信号投递到出错线程。可以通过方法signal(x, SignalHandler)
来捕获signal
NSException
:应用级异常,它是未被捕获的Objective-C
异常,导致程序向自身发送了SIGABRT
信号而崩溃,是app自己可控的,对于未捕获的Objective-C
异常,是可以通过try catch
来捕获的,或者通过NSSetUncaughtExceptionHandler()
机制来捕获
2. Mach异常与 Unix 信号
Mach
是一个微内核
,旨在提供基本的进程间通信功能。
XNU
是一个混合内核,由Mach
微内核和更传统(“单片”)BSD unix内核
的组件组成。它还包括在运行时加载内核扩展的功能(添加功能,设备驱动程序等)
Darwin
是一个Unix
操作系统,由XNU
内核以及各种开源实用程序,库等组成。
OS X
是Darwin
,加上许多专有组件,最着名的是它的图形界面API。
Mach微内核中有几个基础概念:
-
Tasks
,拥有一组系统资源的对象,允许"thread"在其中执行。 -
Threads
,执行的基本单位,拥有task的上下文,并共享其资源。 -
Ports
,task之间通讯的一组受保护的消息队列;task可对任何port发送/接收数据。 -
Message
,有类型的数据对象集合,只可以发送到port。
Mach 异常是指最底层的内核级异常,每个 thread task host都有一个异常端口数组,Mach 的部分 API 暴露给了用户态,用户态的开发者可以直接通过 Mach API 设置 thread task host 的异常端口,来捕获 Mach 异常,抓取 Crash 事件:
所有 Mach 异常未处理,它将在 host 层被 ux_exception转换为相应的 Unix 信号,并通过 threadsignal 将信号投递到出错的线程。iOS 中的 POSIX API 就是通过 Mach 之上的 BSD 层实现的。
我们看到Crash 日志中,Exception Type 项通常会包含两个元素:Mach 异常和 Unix 信号:
Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype: KERN_INVALID_ADDRESS at 0x041a6f3
EXC_BAD_ACCESS (SIGSEGV)
表示的意思是:Mach 层的EXC_BAD_ACCESS异常,在 host 层被转换成 SIGSEGV 信号投递到出错的线程,既然最终以信号的方式投递到出错的线程,那么就可以通过注册signalHandler来捕获信号:
signal(SIGSEGV,signalHandler);
3. Objective-C Exception
比如我们经常遇到的数组越界,数组插入 nil,都是属于此种类型,主要包含以下几类:
NSInvalidArgumentException
非法参数异常NSRangeException
数组越界异常NSGenericException
这个异常最容易出现在foreach操作中,在for in循环中如果修改所遍历的数组,无论你是add或remove,都会出错NSInternalInconsistencyException
不一致导致出现的异常
比如NSDictionary当做NSMutableDictionary来使用,从他们内部的机理来说,就会产生一些错误NSFileHandleOperationException
处理文件时的一些异常,最常见的还是存储空间不足的问题NSMallocException
这也是内存不足的问题,无法分配足够的内存空间
4. 异常的捕获
4.1 Objective-C Exception的捕获
对于Objective-C Exception
的捕获,系统提供了NSSetUncaughtExceptionHandler()
方法,可以注册一个方法,监听 OC 的异常,示例代码如下:
// 用于记录之前的崩溃回调函数
static NSUncaughtExceptionHandler *previousUncaughtExceptionHandler = NULL;
@implementation CrashUncaughtExceptionHandler
#pragma mark - Register
+ (void)registerHandler {
// 有可能其他 SDK 也有异常处理的方法,这里先存储一下
previousUncaughtExceptionHandler = NSGetUncaughtExceptionHandler();
// 注册异常回调函数
NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);
}
#pragma mark - Private
// 崩溃时的回调函数
static void UncaughtExceptionHandler(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目录
// 调用之前崩溃的回调函数
if (previousUncaughtExceptionHandler) {
previousUncaughtExceptionHandler(exception);
}
// 杀掉程序,这样可以防止同时抛出的SIGABRT被SignalException捕获
kill(getpid(), SIGKILL);
}
@end
代码很简单,就是用系统提供的方法注册一个异常发生时的回调函数,但是需要注意的是如果同时有多方注册了异常处理程序,后注册的需要通过NSGetUncaughtExceptionHandler
将之前别人注册的 handler
取出并备份,在自己的 handler
处理完之后,再调用别人的handler
,否则之前注册过的日志手机服务写出的Crash
日志就不起作用了。
另外我们看到在最后调用了kill(getpid(),SIGKILL)
杀死进程,这是因为没有 catch 异常,就会调用obj_exception_throw,然后调用 c 的abort()函数,这个函数会发送SIGABRT信号,如果同时也有SignalException异常捕获,那么就会被捕获两次,所以直接杀死进程。
4.2 Unix信号
捕获 Mach
异常或者 Unix
信号都可以抓到crash
事件,那么这两种方式哪个更好呢?
优选 Mach
异常,因为 Mach
异常处理会先于 Unix
信号处理发生,如果 Mach
异常的 handler
让程序exit
了,那么 Unix
信号就永远不会到达这个进程了。转换Unix
信号是为了兼容POSIX
标准,这样不必了解 Mach
内核也可以通过 Unix 信号
的方式来兼容开发。
因为硬件产生的信号(通过 CPU 陷阱)被 Mach 层捕获,然后才转换为对应的 Unix 信号,苹果为了统一机制,于是操作系统和用户产生的信号(通过调用kill和pthread_kill)也首先沉下来被转换为Mach异常,再转换为Unix信号。
Unix Signal 其实是由 Mach port 抛出的信号转化的,那么都有哪些信号呢?
SIGHUP
本信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联。SIGINT
程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程。SIGQUIT
和SIGINT类似, 但由QUIT字符(通常是Ctrl-)来控制. 进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。SIGABRT
调用abort函数生成的信号。
SIGABRT is a BSD signal sent by an application to itself when an NSException or obj_exception_throw is not caught.SIGBUS
非法地址, 包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数, 但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间)。SIGFPE
在发生致命的算术运算错误时发出. 不仅包括浮点运算错误, 还包括溢出及除数为0等其它所有的算术的错误。SIGKILL
用来立即结束程序的运行. 本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号。SIGSEGV
试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据.SIGPIPE
管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。
和NSSetUncaughtExceptionHandler
的情况类似,设置过的Mach
异常端口和信号处理程序也有可能被干掉,导致无法捕获Crash
事件,所以我们也需要先保存之前的处理:
#import <execinfo.h>
typedef void (*SignalHandler)(int signal, siginfo_t *info, void *context);
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;
@implementation DoraemonCrashSignalExceptionHandler
#pragma mark - Register
// 注册回调处理
+ (void)registerHandler {
// 先保存之前的
[self backupOriginalHandler];
[self signalRegister];
}
+ (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;
}
}
+ (void)signalRegister {
SignalRegister(SIGABRT);
SignalRegister(SIGBUS);
SignalRegister(SIGFPE);
SignalRegister(SIGILL);
SignalRegister(SIGPIPE);
SignalRegister(SIGSEGV);
SignalRegister(SIGSYS);
SignalRegister(SIGTRAP);
}
#pragma mark - Private
#pragma mark Register Signal
static void SignalRegister(int signal) {
struct sigaction action;
action.sa_sigaction = DoraemonSignalHandler;
action.sa_flags = SA_NODEFER | SA_SIGINFO;
sigemptyset(&action.sa_mask);
sigaction(signal, &action, 0);
}
#pragma mark SignalCrash Handler
static void SignalHandler(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"];
// void* callstack[128];
// int i, frames = backtrace(callstack, 128);
// char** strs = backtrace_symbols(callstack, frames);
// for (i = 0; i <frames; ++i) {
// [mstr appendFormat:@"%s\n", strs[I]];
// }
// 这里过滤掉第一行日志
// 因为注册了信号崩溃回调方法,系统会来调用,将记录在调用堆栈上,因此此行日志需要过滤掉
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目录
ClearSignalRigister();
// 调用之前崩溃的回调函数
previousSignalHandler(signal, info, context);
kill(getpid(), SIGKILL);
}
#pragma mark Signal To Name
static NSString *signalName(int signal) {
NSString *signalName;
switch (signal) {
case SIGABRT:
signalName = @"SIGABRT";
break;
case SIGBUS:
signalName = @"SIGBUS";
break;
case SIGFPE:
signalName = @"SIGFPE";
break;
case SIGILL:
signalName = @"SIGILL";
break;
case SIGPIPE:
signalName = @"SIGPIPE";
break;
case SIGSEGV:
signalName = @"SIGSEGV";
break;
case SIGSYS:
signalName = @"SIGSYS";
break;
case SIGTRAP:
signalName = @"SIGTRAP";
break;
default:
break;
}
return signalName;
}
#pragma mark Previous Signal
static void previousSignalHandler(int signal, siginfo_t *info, void *context) {
SignalHandler previousSignalHandler = NULL;
switch (signal) {
case SIGABRT:
previousSignalHandler = previousABRTSignalHandler;
break;
case SIGBUS:
previousSignalHandler = previousBUSSignalHandler;
break;
case SIGFPE:
previousSignalHandler = previousFPESignalHandler;
break;
case SIGILL:
previousSignalHandler = previousILLSignalHandler;
break;
case SIGPIPE:
previousSignalHandler = previousPIPESignalHandler;
break;
case SIGSEGV:
previousSignalHandler = previousSEGVSignalHandler;
break;
case SIGSYS:
previousSignalHandler = previousSYSSignalHandler;
break;
case SIGTRAP:
previousSignalHandler = previousTRAPSignalHandler;
break;
default:
break;
}
if (previousSignalHandler) {
previousSignalHandler(signal, info, context);
}
}
#pragma mark Clear
static void ClearSignalRigister() {
signal(SIGSEGV,SIG_DFL);
signal(SIGFPE,SIG_DFL);
signal(SIGBUS,SIG_DFL);
signal(SIGTRAP,SIG_DFL);
signal(SIGABRT,SIG_DFL);
signal(SIGILL,SIG_DFL);
signal(SIGPIPE,SIG_DFL);
signal(SIGSYS,SIG_DFL);
}
@end
5. 总结
目前看到DoraemonKit
中对于Crash
的收集就是上面的代码,对于mach 内核
了解属实不够,所以一些名词解释暂未补充。先按照他人的文章大概记录下来,有个初步认识,之后慢慢完善。