我們都知道iOS的崩潰信息收集,市面上有很多的三方sdk可供選擇伴逸,而系統(tǒng)對于崩潰的處理handler提供了一個方法去設(shè)置缠沈,于是就有疑問,假如項目想接入多個sdk去采集崩潰信息错蝴,會不會只有一個生效了洲愤?如何讓不同的sdk設(shè)置的handler都執(zhí)行了
1. 崩潰的收集
1.1 設(shè)置多個異常處理handler看看效果
如下所示,我們調(diào)用系統(tǒng)提供的異常處理設(shè)置方法顷锰,我們先后設(shè)置2個handler柬赐,當(dāng)發(fā)生崩潰的時候查看異常情況的調(diào)用情況
NSSetUncaughtExceptionHandler(&HCUncaughtExceptionHandles);
NSSetUncaughtExceptionHandler(&HCAnotherUncaughtExceptionHandles);
// 測試異常代碼
- (void)testException {
NSArray *array = [NSArray arrayWithObjects:@"1", @"2", nil];
__unused NSString *testString = array[2];
}
結(jié)果發(fā)現(xiàn)后設(shè)置的handler生效了,第一個設(shè)置的沒有調(diào)用官紫,這就是被覆蓋了肛宋;假如我們集成了多個收集崩潰的sdk州藕,那么不就只有一個生效了?酝陈?
假如sdk都不做任何處理直接設(shè)置handler的話那么就會有這個問題床玻,我們看看集成Bugly然后設(shè)置自己定義的handler看看表現(xiàn)如何
1.2 集成bugly,再設(shè)置自定義的異常處理handler看看效果
NSSetUncaughtExceptionHandler(&HCUncaughtExceptionHandles);
//NSSetUncaughtExceptionHandler(&HCAnotherUncaughtExceptionHandles);
[Bugly startWithAppId:@"這里替換appid"];
可以看到當(dāng)異常觸發(fā)的時候Bugly的和我們自己設(shè)置的handler都觸發(fā)了后添,看來Bugly還是很友好的做了這種兼容笨枯,優(yōu)秀的sdk就該如此設(shè)計
2. 怎么做到多個Handler的采集
由于系統(tǒng)提供的NSSetUncaughtExceptionHandler的設(shè)置方法,內(nèi)部只會保存一個handler函數(shù)遇西,所以會存在不做處理的情況下覆蓋掉之前設(shè)置的
2.1 分析源碼
從源碼中也可以得到驗證
-
_objc_init中進行異常初始化馅精,設(shè)置為_objc_terminate函數(shù)
exception_init(); void exception_init(void) { old_terminate = std::set_terminate(&_objc_terminate); }
-
_objc_terminate函數(shù)內(nèi)部會對OC的異常調(diào)用foundation的uncaught_handler(如果設(shè)置了的話)
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就是一個函數(shù)指針,當(dāng)設(shè)置多次就會覆蓋前面的設(shè)置粱檀;假如系統(tǒng)設(shè)置這個handler是一個指針集合洲敢,那么設(shè)置多個就存儲多個,調(diào)用的時候去集合中取出回調(diào)調(diào)用茄蚯,理論上是可行的压彭。
現(xiàn)在系統(tǒng)設(shè)計就是一個回調(diào)函數(shù),那么我們?nèi)绾巫龅蕉鄠€回調(diào)都會被調(diào)用了渗常,上面看Bugly是可以做到的壮不,而且看調(diào)用堆棧有個g_BLYPreviousUncaughtExceptionHandler
函數(shù),猜測Bugly是用這個函數(shù)存儲了別人設(shè)置的handler
2.2 Bugly+設(shè)置多個自定義handler查看效果
設(shè)置多個handlers
[Bugly startWithAppId:@"這里替換appid"];
NSSetUncaughtExceptionHandler(&HCUncaughtExceptionHandles);
NSSetUncaughtExceptionHandler(&HCAnotherUncaughtExceptionHandles);
可以看到我們自定義設(shè)置了2個皱碘,結(jié)果后設(shè)置的執(zhí)行了询一,沒有2個都執(zhí)行;大膽猜測Bugly是用一個函數(shù)指針保存了設(shè)置的handler癌椿,在異常處理的時候調(diào)用這個保存的handler健蕊,而且也是只保存最近設(shè)置的那個;這里還有優(yōu)化的空間啊
2.3 自己實現(xiàn)該功能
系統(tǒng)動態(tài)庫的一些C函數(shù)踢俄,它在運行時進行符號的綁定確定函數(shù)的調(diào)用地址缩功,那么我們就可以用fishhook來hook系統(tǒng)的函數(shù)
2.3.1 hook系統(tǒng)設(shè)置handler的函數(shù)
- 定義數(shù)據(jù)結(jié)構(gòu)存儲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系統(tǒng)的NSSetUncaughtExceptionHandler函數(shù)
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 存儲別人設(shè)置的handler函數(shù)指針
這里用了個全局的hashmap去存儲的,當(dāng)設(shè)置異常handler的時候可以看到執(zhí)行了我們hook的方法MineSetUncaughtExceptionHandler
都办,我們在這里進行handler的存儲
測試代碼:這里我們測試自己定義的2個handler
//[Bugly startWithAppId:@"這里替換為appId"];
NSSetUncaughtExceptionHandler(&HCUncaughtExceptionHandles);
NSSetUncaughtExceptionHandler(&HCAnotherUncaughtExceptionHandles);
Debug查看如下圖嫡锌,我們將設(shè)置的handler存儲起來了,并將系統(tǒng)的異常的回調(diào)handler設(shè)置為HCUncaughtExceptionHandles
2.3.3 異沉斩ぃ回調(diào)的時候世舰,執(zhí)行存儲的異常handlers
我們在hook系統(tǒng)的設(shè)置handler的方法的時候,設(shè)置最終的回調(diào)處理為HCUncaughtExceptionHandles
槽卫,那么我們就在該回調(diào)函數(shù)中處理其他注冊的handler的觸發(fā)邏輯
代碼如下:
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);
}
}];
// 自己的異常處理邏輯
}
異常發(fā)生查看效果
至此已經(jīng)實現(xiàn)了我們開始的需求如何同時存在多個異常處理的handler
同理signal的異常也可以通過hook signal函數(shù)去設(shè)計實現(xiàn)跟压;這里就舉個例子針對Exception的回調(diào)