APM 视角下的 NSException

596 阅读6分钟

异常处理

OC 会通过 objc_exception_throw 方法,抛出 NSException 类型的异常,这个方法内最终也会使用 __cxa_throw 抛出异常,因此本质上 NSException 和 C++  exception 的异常处理流程没什么区别。

可以参考这篇文章,juejin.cn/post/733192… 里面详细梳理了 C++ 的异常处理流程。

__cxa_throw 方法内会判断当前异常是否会被 catch,是则跳转到对应的 catch block,否则执行 terminate 触发 abort,iOS 主线程,以及 dispatch queue 内执行的代码所抛出的异常都会被系统方法 catch 住后重新 rethrow 或者执行 terminate。因此未捕获 NSException 触发的崩溃,会涉及到两部分堆栈,崩溃堆栈和抛异常堆栈。

异常堆栈

如图所示 崩溃堆栈 为 Thread 1 线程堆栈,抛异常堆栈 为 original exception backtrace,通常崩溃看板展示的是 original exception backtrace。Apple 提供 MetricKit 从信号里面处理NSException 异常,展示崩溃时的堆栈,这个堆栈对我们排查问题没有任何帮助。

original exception backtrace 是从 NSException 属性 callStack 获取的。NSException 相关属性有两个 callStackReturnAddresses 存 pc,callStackSymbols 存符号化后的信息。

@property (readonly, copy) NSArray<NSNumber *> *callStackReturnAddresses API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
@property (readonly, copy) NSArray<NSString *> *callStackSymbols API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0));

在 apm 内,对未捕获 NSException 我们上报的是 callStackReturnAddresses,离线解析出函数名,同时可以获取到文件名和行号。callStackSymbols 内的 item 是个文本信息,好处是有可能能直接取到函数名,但是结构化上报需要用正则去匹配里面的关键信息,处理不够简洁。

(lldb) po [exception callStackSymbols]
<_NSCallStackArray 0x302b54540>(
0   CoreFoundation                      0x0000000196330f2c 115C88DD-E371-38ED-8318-79C0298CDA9F + 540460,
1   libobjc.A.dylib                     0x000000018e1d72b8 objc_exception_throw + 60,
2   CoreFoundation                      0x000000019642f6dc 115C88DD-E371-38ED-8318-79C0298CDA9F + 1582812,
3   Ekko_Example                        0x000000010276c28c -[EkkoViewController clickCrashButton:] + 112,
4   UIKitCore                           0x0000000198931e04 2F441C19-B156-39F2-97F7-EB6F889365F4 + 4173316,

NSException 的 original exception backtrace 在系统的预处理方法里面进行赋值。

预处理方法

抛出异常时,会执行 objc_exception_throw,这个方法内会执行一个预处理方法 exception_preprocessor,preprocessor 可以说是一个宝藏方法,给我们提供了一个抛异常时的勾子方法,借助这个方法既可以获取到抛异常的上下文信息,也可以做异常的全局兜底。

void objc_exception_throw(id obj)
{
    struct objc_exception *exc = (struct objc_exception *)
        __cxa_allocate_exception(sizeof(struct objc_exception));

    obj = (*exception_preprocessor)(obj);

默认的预处理方法是_objc_default_exception_preprocessor,什么都不做,期望被覆写。Expected to be overridden by Foundation。

static objc_exception_preprocessor exception_preprocessor = _objc_default_exception_preprocessor;

/***********************************************************************
* _objc_default_exception_preprocessor
* Default exception preprocessor. Expected to be overridden by Foundation.
**********************************************************************/
static id _objc_default_exception_preprocessor(id exception)
{
    return exception;
}

exception_preprocessor 可以通过 objc_setExceptionPreprocessor 覆写:

/***********************************************************************
* objc_setExceptionPreprocessor
* Set a handler for preprocessing Objective-C exceptions. 
* Returns the previous handler. 
**********************************************************************/
objc_exception_preprocessor
objc_setExceptionPreprocessor(objc_exception_preprocessor fn)
{
    objc_exception_preprocessor result = exception_preprocessor;
    exception_preprocessor = fn;
    return result;
}

对 objc_setExceptionPreprocessor 加符号断点,在 dyld 加载的时候会设置这个预处理方法为 CoreFoudation 内的 __exceptionPreprocess。

__exceptionPreprocess 方法内会设置 NSException 的堆栈,___exceptionPreprocess 反汇编的代码,核心逻辑是通过 NSThread 的 callStackReturnAddressess 和 callStackSymbols 获取堆栈信息赋值给 NSException。

int ___exceptionPreprocess(int arg0) {
    var_20 = r22;
    stack[-40] = r21;
    r31 = r31 + 0xffffffffffffffd0;
    var_10 = r20;
    stack[-24] = r19;
    saved_fp = r29;
    stack[-8] = r30;
    r19 = arg0;
  	// 判断是否是 nsexception 类型
    if (_objectIsKindOfClass(arg0, *0x1df35fef0) != 0x0) {
      		// 取 nsexception 偏移量 0x20 指针
            r20 = *(r19 + 0x20); // NSMutableDictionary
            if (r20 != 0x0) { // 指针不为空
              		// NSMutableDictionary 是否存在 callStackReturnAddressess callStackSymbols
                    if (_objc_msgSend$objectForKey:() == 0x0 && _objc_msgSend$objectForKey:() == 0x0) {
                            _objc_msgSend$userInfo();
                            _objc_msgSend$objectForKey:(); // 判断  NSExceptionOmitCallstacks
                            if ((_objc_msgSend$boolValue() & 0x1) == 0x0) {
			                              // 获取当前线程的堆栈信息
                                    ___CFLookUpClass("NSThread");
                                    r22 = _objc_msgSend$callStackReturnAddresses();
                                    r21 = _objc_msgSend$callStackSymbols();
                                    if (r22 != 0x0) {
                                            _objc_msgSend$setObject:forKey:();
                                    }
                                    if (r21 != 0x0) {
                                            _objc_msgSend$setObject:forKey:();
                                    }
                            }
                    }
            }
            else {
                    ___CFLookUpClass("NSMutableDictionary");
                    r0 = loc_18dbbd1ac();
                    r20 = r0;
                    *(r19 + 0x20) = r0;
                    _objc_msgSend$userInfo();
                    _objc_msgSend$objectForKey:();
                    if ((_objc_msgSend$boolValue() & 0x1) == 0x0) {
                            ___CFLookUpClass("NSThread");
                            r22 = _objc_msgSend$callStackReturnAddresses();
                            r21 = _objc_msgSend$callStackSymbols();
                            if (r22 != 0x0) {
                                    _objc_msgSend$setObject:forKey:();
                            }
                            if (r21 != 0x0) {
                                    _objc_msgSend$setObject:forKey:();
                            }
                    }
            }
    }
    r0 = r19;
    return r0;
}

获取抛异常上下文

未捕获 NSException 导致的崩溃堆栈通常都是比较固定的,一种是异常被主线程的 runloop(多个方法都存在 try catch) 捕获,然后重新 rethrow。

一种是被 dispatch 捕获然后直接执行 terminate。

异常被捕获,然后重新抛出或执行 terminate 到未捕获异常的监听流程里面,虽然 NSException 对象自身保留了堆栈信息,但是抛异常的上下文已经丢失,抛异常时的部分信息已经无法获取,比如寄存器信息。

 

对于 NSException 如何获取抛异常时的上下文信息呢?

上文提到的,通过 objc_setExceptionPreprocessor 方法,设置我们自定义的 preprocessor 方法。在自定义的 preprocessor 方法内判断异常是否会被捕获,不会被捕获或者被 runloop、disapatch 捕获的场景下,记录上下文信息,在触发崩溃时匹配之前记录的信息。  

监听未捕获 NSException

未捕获 NSException 导致的崩溃,都会执行 _objc_terminate,该方法通过 set_terminate 设置:

/***********************************************************************
* _objc_terminate
* Custom std::terminate handler.
*
* The uncaught exception callback is implemented as a std::terminate handler. 
* 1. Check if there's an active exception
* 2. If so, check if it's an Objective-C exception
* 3. If so, call our registered callback with the object.
* 4. Finally, call the previous terminate handler.
**********************************************************************/
static void (*old_terminate)(void) = nil;
static void _objc_terminate(void)
{
    if (PrintExceptions) {
        _objc_inform("EXCEPTIONS: terminating");
    }

    if (! __cxa_current_exception_type()) {
        // No current exception.
        (*old_terminate)();
    }
    else {
        // There is a current exception. Check if it's an objc exception.
        @try {
            __cxa_rethrow();
        } @catch (id e) {
            // It's an objc object. Call Foundation's handler, if any.
            (*uncaught_handler)((id)e);
            (*old_terminate)();
        } @catch (...) {
            // It's not an objc object. Continue to C++ terminate.
            (*old_terminate)();
        }
    }
}

对于 OC 异常类型,会先执行 uncaught_handler。uncaught_handler 通过 objc_setUncaughtExceptionHandler 设置

/***********************************************************************
* objc_setUncaughtExceptionHandler
* Set a handler for uncaught Objective-C exceptions. 
* Returns the previous handler. 
**********************************************************************/
objc_uncaught_exception_handler 
objc_setUncaughtExceptionHandler(objc_uncaught_exception_handler fn)
{
    objc_uncaught_exception_handler result = uncaught_handler;
    uncaught_handler = fn;
    return result;
}

dyld 初始化阶段会把 uncaught_handler 设置为 Corefoudation 内的 __handleUncaughtException。  

了解过未捕获 NSException 的,都会知道这个方法  NSSetUncaughtExceptionHandler,设置未捕获异常的回调。我之前以为 NSSetUncaughtExceptionHandler 会执行 objc_setUncaughtExceptionHandler 设置 uncaught_handler。实际上不是这样的, 执行 NSSetUncaughtExceptionHandler,会把 Handler 设置到一个全局变量 0x128 的位置。

__handleUncaughtException 执行时,会去取 NSSetUncaughtExceptionHandler 设置的 handler 执行。

adrp 的第二个参数值虽然不同,但是计算后写入 x8 的值是相等的。

对于未捕获 NSException 的处理,会经过以下 3 个方法:

  1. 未捕获异常会先执行 std::termiante。

    系统通过 set_terminate 设置 terminate == _objc_terminate

  2. _objc_terminate 内执行 uncatch_handler

    系统通过 objc_setUncaughtExceptionHandler 设置 uncatch_handler == __handleUncaughtException

  3. __handleUncaughtException 执行 NSSetUncaughtExceptionHandler 设置的 handler

除了我们熟知的 NSSetUncaughtExceptionHandler,通过 objc_setUncaughtExceptionHandler 和 set_terminate 都能设置未捕获 NSException 的监听入口。目前对比这三种方法,我认为 apm 的处理的最佳时机是在 terminate 回调里面,处理的时机越早,受到的干扰也就越少,获取未捕获异常的成功率也就越高。