likes
comments
collection
share

APM 视角下的 NSException本文介绍 APM 崩溃监控中对 NSException 的捕获,以及如何扩展,对

作者站长头像
站长
· 阅读数 26

异常处理

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

APM 视角下的 NSException本文介绍 APM 崩溃监控中对 NSException 的捕获,以及如何扩展,对

__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 异常,展示崩溃时的堆栈,这个堆栈对我们排查问题没有任何帮助。 APM 视角下的 NSException本文介绍 APM 崩溃监控中对 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。 APM 视角下的 NSException本文介绍 APM 崩溃监控中对 NSException 的捕获,以及如何扩展,对

__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。 APM 视角下的 NSException本文介绍 APM 崩溃监控中对 NSException 的捕获,以及如何扩展,对

一种是被 dispatch 捕获然后直接执行 terminate。 APM 视角下的 NSException本文介绍 APM 崩溃监控中对 NSException 的捕获,以及如何扩展,对

异常被捕获,然后重新抛出或执行 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。 APM 视角下的 NSException本文介绍 APM 崩溃监控中对 NSException 的捕获,以及如何扩展,对 

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

APM 视角下的 NSException本文介绍 APM 崩溃监控中对 NSException 的捕获,以及如何扩展,对

__handleUncaughtException 执行时,会去取 NSSetUncaughtExceptionHandler 设置的 handler 执行。 APM 视角下的 NSException本文介绍 APM 崩溃监控中对 NSException 的捕获,以及如何扩展,对

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 回调里面,处理的时机越早,受到的干扰也就越少,获取未捕获异常的成功率也就越高。

转载自:https://juejin.cn/post/7406148010038165556
评论
请登录