我们都知道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];
}
结果发现后设置的handler生效了,第一个设置的没有调用,这就是被覆盖了;假如我们集成了多个收集崩溃的sdk,那么不就只有一个生效了??
假如sdk都不做任何处理直接设置handler的话那么就会有这个问题,我们看看集成Bugly然后设置自己定义的handler看看表现如何
1.2 集成bugly,再设置自定义的异常处理handler看看效果
NSSetUncaughtExceptionHandler(&HCUncaughtExceptionHandles);
//NSSetUncaughtExceptionHandler(&HCAnotherUncaughtExceptionHandles);
[Bugly startWithAppId:@"这里替换appid"];
可以看到当异常触发的时候Bugly的和我们自己设置的handler都触发了,看来Bugly还是很友好的做了这种兼容,优秀的sdk就该如此设计
2. 怎么做到多个Handler的采集
由于系统提供的NSSetUncaughtExceptionHandler的设置方法,内部只会保存一个handler函数,所以会存在不做处理的情况下覆盖掉之前设置的
2.1 分析源码
从源码中也可以得到验证
-
_objc_init中进行异常初始化,设置为_objc_terminate函数
exception_init(); void exception_init(void) { old_terminate = std::set_terminate(&_objc_terminate); }
-
_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)(); } } }
- 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);
可以看到我们自定义设置了2个,结果后设置的执行了,没有2个都执行;大胆猜测Bugly是用一个函数指针保存了设置的handler,在异常处理的时候调用这个保存的handler,而且也是只保存最近设置的那个;这里还有优化的空间啊
2.3 自己实现该功能
系统动态库的一些C函数,它在运行时进行符号的绑定确定函数的调用地址,那么我们就可以用fishhook来hook系统的函数
2.3.1 hook系统设置handler的函数
- 定义数据结构存储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
- 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
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);
}
}];
// 自己的异常处理逻辑
}
异常发生查看效果
至此已经实现了我们开始的需求如何同时存在多个异常处理的handler
同理signal的异常也可以通过hook signal函数去设计实现;这里就举个例子针对Exception的回调