iOS开发

iOS崩溃采集如何兼容多个异常回调

2020-05-30  本文已影响0人  jayhe

我们都知道iOS的崩溃信息收集,市面上有很多的三方sdk可供选择,而系统对于崩溃的处理handler提供了一个方法去设置,于是就有疑问,假如项目想接入多个sdk去采集崩溃信息,会不会只有一个生效了?如何让不同的sdk设置的handler都执行了

1. 崩溃的收集

1.1 设置多个异常处理handler看看效果

如下所示,我们调用系统提供的异常处理设置方法,我们先后设置2个handler,当发生崩溃的时候查看异常情况的调用情况

NSSetUncaughtExceptionHandler(&HCUncaughtExceptionHandles);
NSSetUncaughtExceptionHandler(&HCAnotherUncaughtExceptionHandles);

// 测试异常代码
- (void)testException {
    NSArray *array = [NSArray arrayWithObjects:@"1", @"2", nil];
    __unused NSString *testString = array[2];
}
图片.png

结果发现后设置的handler生效了,第一个设置的没有调用,这就是被覆盖了;假如我们集成了多个收集崩溃的sdk,那么不就只有一个生效了??

假如sdk都不做任何处理直接设置handler的话那么就会有这个问题,我们看看集成Bugly然后设置自己定义的handler看看表现如何

1.2 集成bugly,再设置自定义的异常处理handler看看效果

 NSSetUncaughtExceptionHandler(&HCUncaughtExceptionHandles);
 //NSSetUncaughtExceptionHandler(&HCAnotherUncaughtExceptionHandles);
 [Bugly startWithAppId:@"这里替换appid"];
图片.png

可以看到当异常触发的时候Bugly的和我们自己设置的handler都触发了,看来Bugly还是很友好的做了这种兼容,优秀的sdk就该如此设计

2. 怎么做到多个Handler的采集

由于系统提供的NSSetUncaughtExceptionHandler的设置方法,内部只会保存一个handler函数,所以会存在不做处理的情况下覆盖掉之前设置的

2.1 分析源码

从源码中也可以得到验证

  1. _objc_init中进行异常初始化,设置为_objc_terminate函数

    exception_init();
    void exception_init(void)
    {
        old_terminate = std::set_terminate(&_objc_terminate);
    }
    
    
    
  2. _objc_terminate函数内部会对OC的异常调用foundation的uncaught_handler(如果设置了的话)

    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)();
            }
        }
    }
    
  1. objc-exception中提供了读写uncaught_handler的方法
static objc_uncaught_exception_handler uncaught_handler = _objc_default_uncaught_exception_handler;
objc_uncaught_exception_handler
objc_setUncaughtExceptionHandler(objc_uncaught_exception_handler fn)
{
    objc_uncaught_exception_handler result = uncaught_handler;
    uncaught_handler = fn;
    return result;
}

可以看到uncaught_handler就是一个函数指针,当设置多次就会覆盖前面的设置;假如系统设置这个handler是一个指针集合,那么设置多个就存储多个,调用的时候去集合中取出回调调用,理论上是可行的。

现在系统设计就是一个回调函数,那么我们如何做到多个回调都会被调用了,上面看Bugly是可以做到的,而且看调用堆栈有个g_BLYPreviousUncaughtExceptionHandler函数,猜测Bugly是用这个函数存储了别人设置的handler

2.2 Bugly+设置多个自定义handler查看效果

设置多个handlers

[Bugly startWithAppId:@"这里替换appid"];
NSSetUncaughtExceptionHandler(&HCUncaughtExceptionHandles);
NSSetUncaughtExceptionHandler(&HCAnotherUncaughtExceptionHandles);
图片.png

可以看到我们自定义设置了2个,结果后设置的执行了,没有2个都执行;大胆猜测Bugly是用一个函数指针保存了设置的handler,在异常处理的时候调用这个保存的handler,而且也是只保存最近设置的那个;这里还有优化的空间啊

2.3 自己实现该功能

系统动态库的一些C函数,它在运行时进行符号的绑定确定函数的调用地址,那么我们就可以用fishhook来hook系统的函数

2.3.1 hook系统设置handler的函数
  1. 定义数据结构存储handler指针
@interface HCHandler : NSObject {
    @package
    NSUncaughtExceptionHandler *handler;
}

@end

@implementation HCHandler
  
- (BOOL)isEqual:(HCHandler *)object {
    if (self == object) {
        return YES;
    }
    
    return (self->handler) == (object->handler);
}

- (NSUInteger)hash {
    //NSLog(@"%ld", (NSUInteger)(self->handler));
    return (NSUInteger)(self->handler);
}

@end
  1. hook系统的NSSetUncaughtExceptionHandler函数
static void (*SystemNSSetUncaughtExceptionHandler)(NSUncaughtExceptionHandler * _Nullable handler);
static NSHashTable *_handlers;

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _handlers = [NSHashTable weakObjectsHashTable];
        struct rebinding rebindingHandler = {};
        rebindingHandler.name = "NSSetUncaughtExceptionHandler";
        rebindingHandler.replacement = (void *)MineSetUncaughtExceptionHandler;
        rebindingHandler.replaced = (void **)&SystemNSSetUncaughtExceptionHandler;
        struct rebinding rebindings[] = {rebindingHandler};
        rebind_symbols(rebindings, 1);
    });
}

void MineSetUncaughtExceptionHandler(NSUncaughtExceptionHandler * _Nullable handler) {
    if (handler == HCUncaughtExceptionHandles) { // 如果handler是我们自己的handler就不加入到hashmap中
        SystemNSSetUncaughtExceptionHandler(&HCUncaughtExceptionHandles);
    } else {
        HCHandler *handlerObj = [HCHandler new];
           handlerObj->handler = handler;
           if (![_handlers containsObject:handlerObj]) {
               [_handlers addObject:handlerObj];
           }
    }
}
2.3.2 存储别人设置的handler函数指针

这里用了个全局的hashmap去存储的,当设置异常handler的时候可以看到执行了我们hook的方法MineSetUncaughtExceptionHandler,我们在这里进行handler的存储

测试代码:这里我们测试自己定义的2个handler

//[Bugly startWithAppId:@"这里替换为appId"];
NSSetUncaughtExceptionHandler(&HCUncaughtExceptionHandles);
NSSetUncaughtExceptionHandler(&HCAnotherUncaughtExceptionHandles);

Debug查看如下图,我们将设置的handler存储起来了,并将系统的异常的回调handler设置为HCUncaughtExceptionHandles

图片.png
2.3.3 异常回调的时候,执行存储的异常handlers

我们在hook系统的设置handler的方法的时候,设置最终的回调处理为HCUncaughtExceptionHandles,那么我们就在该回调函数中处理其他注册的handler的触发逻辑

代码如下:

oid HCUncaughtExceptionHandles(NSException *exception) {
    NSArray<HCHandler *> *handlers = _handlers.allObjects;
    [handlers enumerateObjectsUsingBlock:^(HCHandler * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if (obj->handler) {
            obj->handler(exception);
        }
    }];
    // 自己的异常处理逻辑
}

异常发生查看效果

图片.png

至此已经实现了我们开始的需求如何同时存在多个异常处理的handler

同理signal的异常也可以通过hook signal函数去设计实现;这里就举个例子针对Exception的回调

上一篇下一篇

猜你喜欢

热点阅读