APM 视角下的 NSException本文介绍 APM 崩溃监控中对 NSException 的捕获,以及如何扩展,对
异常处理
OC 会通过 objc_exception_throw 方法,抛出 NSException 类型的异常,这个方法内最终也会使用 __cxa_throw 抛出异常,因此本质上 NSException 和 C++ exception 的异常处理流程没什么区别。
__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 个方法:
-
未捕获异常会先执行 std::termiante。
系统通过 set_terminate 设置 terminate == _objc_terminate
-
_objc_terminate 内执行 uncatch_handler
系统通过 objc_setUncaughtExceptionHandler 设置 uncatch_handler == __handleUncaughtException
-
__handleUncaughtException 执行 NSSetUncaughtExceptionHandler 设置的 handler
除了我们熟知的 NSSetUncaughtExceptionHandler,通过 objc_setUncaughtExceptionHandler 和 set_terminate 都能设置未捕获 NSException 的监听入口。目前对比这三种方法,我认为 apm 的处理的最佳时机是在 terminate 回调里面,处理的时机越早,受到的干扰也就越少,获取未捕获异常的成功率也就越高。
转载自:https://juejin.cn/post/7406148010038165556