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

我们都知道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的回调

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容