戳我
戳我
文章目录
  1. Crash的捕获与处理
    1. 1 Objective-C异常
      1. 1.1 常见的Objective-C异常
      2. 1.2 捕获Objective-C异常
    2. 2 Mach异常
      1. 2.1 Mach与Unix信号
      2. 2.2 Unix信号
      3. 2.3 Mach异常捕获
      4. 2.4 Unix信号捕获
      5. 2.5 Mach 异常 + Unix 信号
  2. 3 总结

Crash的捕获与处理

Crash的捕获与处理

前面写过一篇iOS开发的崩溃日志分析与异常类型, 这里来聊一下Crash的类型以及如何捕获Crash.

对于常见的Crash而言, 可以分为两类, 一类是Objective-C异常, 另一类是Mach异常.

1 Objective-C异常

Objective-C异常就是指在OC层面(iOS库、第三方库出现错误时)出现的异常. 如何捕获Objective-C异常之前我们先来看下常见的Objective-C异常包括哪些.

1.1 常见的Objective-C异常

Objective-C异常又称为应用级异常, 常见的Objective-C异常包括以下几种:

  1. NSInvalidArgumentException(非法参数异常) 这类异常的主要原因是没有对于参数的合法性进行校验,最常见的就是传入nil作为参数。例如,NSMutableDictionary添加key为nil的对象,测试代码如下
  2. NSRangeException(越界异常) 这类异常的主要原因是没有对于索引进行合法性的检查,导致索引落在集合数据的合法范围之外。例如,索引超出数组的范围从而导致数组越界的问题
  3. NSGenericException(通用异常) 这类异常最容易出现在foreach操作中,主要原因是在遍历过程中进行了元素的修改。例如,在for in循环中如果修改所遍历的数组则会导致该问题
  4. NSMallocException(内存分配异常) 这类异常的主要原因是无法分配足够的内存空间。例如,分配一块超大的内存空间就会导致此类的异常
  5. NSFileHandleOperationException(文件处理异常) 这类异常的主要原因是对文件进行相关操作时产生了异常,如手机没有足够的存储空间,文件读写权限问题等。例如,对于一个只有读权限的文件进行写操作

1.2 捕获Objective-C异常

在开发过程中,通过添加全局断点, Objective-C异常导致的Crash会在Xcode的控制台输出异常的类型、原因以及调用堆栈.

其他条件下, 可以使用try catch或者NSSetUncaughtExceptionHandler()来捕获.

// 记录之前的崩溃回调函数(防止多个Crash收集工具冲突)
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)"];

    // 调用之前崩溃的回调函数(防止多个Crash收集工具冲突)
    if (previousUncaughtExceptionHandler) {
        previousUncaughtExceptionHandler(exception);
    }
}

2 Mach异常

iOS操作系统的内核部分都是XNU,而Mach就是XNU的微内核核心. Mach的职责主要是进程和线程抽象、虚拟内存管理、任务调度、进程间通信和消息传递机制等.

2.1 Mach与Unix信号

相信大家一定见过这样的错误提示:

Exception Type:         EXC_BAD_ACCESS (SIGSEGV)    
Exception Subtype:      KERN_INVALID_ADDRESS at 0x041a6f3

Apple’s Crash Reporter 记录在设备中的 Crash 日志, Exception Type 项通常会包含两个元素: Mach 异常 和 Unix 信号.

Mach 是一个 XNU 的微内核核心,Mach 异常是指最底层的内核级异常,被定义在 <mach/exception_types.h>下 。每个 thread,task,host 都有一个异常端口数组,Mach 的部分 API 暴露给了用户态,用户态的开发者可以直接通过 Mach API 设置 thread,task,host 的异常端口,来捕获 Mach 异常,抓取 Crash 事件. Mach 异常允许在进程里或进程外处理,处理程序通过Mach RPC调用.

所以, 上面代码里面的异常提醒就代表: Mach 层的EXC_BAD_ACCESS异常,在 host 层被转换成 SIGSEGV 信号投递到出错的线程.

Mach 异常是指最底层的内核级异常。用户态的开发者可以直接通过Mach API设置thread,task,host的异常端口,来捕获Mach异常.

2.2 Unix信号

Unix 信号又称BSD 信号,如果开发者没有捕获Mach异常,则会被host层的方法ux_exception()将异常转换为对应的UNIX信号,并通过方法threadsignal()将信号投递到出错线程。可以通过方法signal(x, SignalHandler)来捕获single.

Unix信号有很多种,详细的定义可以在<sys/signal.h>中找到.
常见的信号的释义可以参考《iOS异常捕获》.

2.3 Mach异常捕获

捕获 Mach 异常或者 Unix 信号都可以抓到 crash 事件, 这两种方式哪个更好呢?优选 Mach 异常,因为 Mach 异常处理会先于 Unix 信号处理发生,如果 Mach 异常的 handler 让程序 exit 了,那么 Unix 信号就永远不会到达这个进程了。转换 Unix 信号是为了兼容更为流行的 POSIX 标准 (SUS 规范),这样不必了解 Mach 内核也可以通过 Unix 信号的方式来兼容开发.

Mach异常捕获

2.4 Unix信号捕获

因为硬件产生的信号 (通过 CPU 陷阱) 被 Mach 层捕获,然后才转换为对应的 Unix 信号;苹果为了统一机制,于是操作系统和用户产生的信号 (通过调用kill和pthread_kill) 也首先沉下来被转换为 Mach 异常,再转换为 Unix 信号

参考捕获Objective-C异常时处理覆盖问题的思路,我们也可以先将已有的异常处理函数进行保存,然后在我们的异常处理函数执行之后,再调用之前保存的异常处理函数.

//注册异常捕获
InstallSignalHandler();

void SignalExceptionHandler(int signal)
{
    NSMutableString *mstr = [[NSMutableString alloc] init];
    [mstr appendString:@"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]];
    }
    [SignalHandler saveCreash:mstr];

}

void InstallSignalHandler(void)
{
    signal(SIGHUP, SignalExceptionHandler);
    signal(SIGINT, SignalExceptionHandler);
    signal(SIGQUIT, SignalExceptionHandler);

    signal(SIGABRT, SignalExceptionHandler);
    signal(SIGILL, SignalExceptionHandler);
    signal(SIGSEGV, SignalExceptionHandler);
    signal(SIGFPE, SignalExceptionHandler);
    signal(SIGBUS, SignalExceptionHandler);
    signal(SIGPIPE, SignalExceptionHandler);
}

2.5 Mach 异常 + Unix 信号

捕获Mach异常或者Unix信号都可以抓到Crash事件, 通常使用了Unix信号方式进行捕获. 主要原因如下:

  1. Mach异常没有比较便利的捕获方式,既然它最终会转化成信号,我们也可以通过捕获信号来捕获Crash事件。
  2. 转换Unix信号是为了兼容更为流行的POSIX标准(SUS规范),这样不必了解Mach内核也可以通过Unix信号的方式来兼容开发。

Unix信号捕获可以参考上一章节内容.

3 总结

  1. 异常分为应用级异常(Obj-C异常)和Mach异常
  2. Obj-C异常使用NSSetUncaughtExceptionHandler()捕获
  3. Mach异常通过Mach API设置thread,task,host的异常端口,来捕获Mach异常
  4. 未被捕获的Mach异常会被转换为Unix信号异常, 苹果将操作系统和用户产生的信号 (通过调用kill和pthread_kill) 也首先沉下来被转换为 Mach 异常, 再转换为 Unix 信号.
  5. Unix异常通过signal(SIGSEGV,signalHandler);方式来捕获
  6. 其他操作系统优选Mach异常, 防止未被转化为Unix信号程序就结束.
  7. iOS基本上都会被转化为Unix异常, 优先考虑通过Unix 信号捕获异常.
  8. 多个crash收集, 注意异常捕获的传递

参考资料:
1.iOS开发中crash常用处理
2.DoKit支持iOS本地crash查看功能
3.iOS异常捕获
4.关于 iOS App 的 Crash 捕获简述
5.漫谈 iOS Crash 收集框架