[iOS] 崩溃类型以及相关收集

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 XDarwin,加上许多专有组件,最着名的是它的图形界面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 事件:

image.png

所有 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 


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 

5. 总结

image.png

目前看到DoraemonKit中对于Crash 的收集就是上面的代码,对于mach 内核了解属实不够,所以一些名词解释暂未补充。先按照他人的文章大概记录下来,有个初步认识,之后慢慢完善。

你可能感兴趣的:([iOS] 崩溃类型以及相关收集)