一、目的
发现很多同学处理exception时都非常简单粗暴,直接加try catch,但是这是一个很不好的习惯。而且本身也可能导致功能无法稳定使用。我们的目的应该是知道问题了,就尽快解决好问题,而不是想着try catch不上报不crash就行了。所以就需要建立一个监控上报啦。
二、理解try_catch
跟进苹果的源码https://opensource.apple.com/source/objc4/objc4-532/runtime/objc-exception.mm.auto.html
void objc_exception_throw(id exception) {
if (!xtab.throw_exc) {
set_default_handlers();
}
if (PrintExceptionThrow) {
_objc_inform("EXCEPTIONS: throwing %p (%s)",
exception, object_getClassName(exception));
void* callstack[500];
int frameCount = backtrace(callstack, 500);
backtrace_symbols_fd(callstack, frameCount, fileno(stderr));
}
OBJC_RUNTIME_OBJC_EXCEPTION_THROW(exception); // dtrace probe to log throw activity.
xtab.throw_exc(exception);
_objc_fatal("objc_exception_throw failed");
}
void objc_exception_try_enter(void *localExceptionData) {
if (!xtab.throw_exc) {
set_default_handlers();
}
xtab.try_enter(localExceptionData);
}
void objc_exception_try_exit(void *localExceptionData) {
if (!xtab.throw_exc) {
set_default_handlers();
}
xtab.try_exit(localExceptionData);
}
id objc_exception_extract(void *localExceptionData) {
if (!xtab.throw_exc) {
set_default_handlers();
}
return xtab.extract(localExceptionData);
}
实际上所有的@try@catch调用都是如上的C函数调用。因此实际的问题也简单了,我们想捕获所有的try_catch的异常,那就在catch里做文章就行了。
三、思路
翻开objc-exception.h头文件,我们发现了一个神奇的函数
OBJC_EXPORT objc_exception_matcher _Nonnull
objc_setExceptionMatcher(objc_exception_matcher _Nonnull fn)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
这个函数看起来会返回一个objc_exception_matcher,看起来有点搞头;
于是定位到该函数源码
/***********************************************************************
* _objc_default_exception_matcher
* Default exception matcher. Expected to be overridden by Foundation.
**********************************************************************/
static int _objc_default_exception_matcher(Class catch_cls, id exception)
{
Class cls;
for (cls = _object_getClass(exception);
cls != NULL;
cls = class_getSuperclass(cls))
{
if (cls == catch_cls) return 1;
}
return 0;
}
static objc_exception_matcher exception_matcher = _objc_default_exception_matcher;
/***********************************************************************
* _objc_default_uncaught_exception_handler
* Default uncaught exception handler. Expected to be overridden by Foundation.
**********************************************************************/
static void _objc_default_uncaught_exception_handler(id exception)
{
}
static objc_uncaught_exception_handler uncaught_handler = _objc_default_uncaught_exception_handler;
/***********************************************************************
* 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_setExceptionMatcher
* Set a handler for matching Objective-C exceptions.
* Returns the previous handler.
**********************************************************************/
objc_exception_matcher
objc_setExceptionMatcher(objc_exception_matcher fn)
{
objc_exception_matcher result = exception_matcher;
exception_matcher = fn;
return result;
}
看完源码,一下子豁然开朗了,原来只要执行了一次objc_setExceptionMatcher就会返回它原始的那个全局静态变量
static objc_exception_matcher exception_matcher = _objc_default_exception_matcher;
只要我们执行一次objc_setExceptionMatcher不就能解决问题吗?然后就是一个简单的hook逻辑了;先执行一次objc_setExceptionMatcher返回了默认matcher,保存该函数执行,并再我们set的新match里回调该函数,一切解决,而实现这一切只需要fishhook就行了。
四、解决问题
具体代码如下:
需要依赖facebook的fishhook
#import
#import "fishhook.h"
#import
#import
#import
//http://www.mirrorservice.org/pub/NetBSD/NetBSD-current/src/external/gpl3/gcc/dist/libobjc/exception.c
//objc_setExceptionMatcher
static objc_exception_matcher (*mtt_origin_objc_setExceptionMatcher)(objc_exception_matcher fn);
static objc_exception_matcher mtt_origin_hook_exception_matcher=NULL;
static dispatch_queue_t reportQueue = NULL;
/**
return 0代表这个异常无法被matcher,而会自动走入throw unhandle exception里.
return 1则标识这个异常可以被safe处理掉,不会触发实际的app crash.会走入try catch的catch里.
*/
static inline int mtt_hook_objc_exception_matcher(Class cls,id exception)
{
if([exception isKindOfClass:[NSException class]])
{
if (reportQueue == NULL) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
reportQueue = dispatch_queue_create("mtt.exceptionreport", DISPATCH_QUEUE_SERIAL);
});
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), reportQueue, ^{
[[CrashReporter sharedInstance] reportException:(NSException*)exception reason:@"@try_catch" extraInfo:nil];
});
}
//防止递归...
if(mtt_origin_hook_exception_matcher!=mtt_hook_objc_exception_matcher)
{
//__exceptionMatch
return mtt_origin_hook_exception_matcher(cls,exception);
}
return 0;
}
objc_exception_matcher mtt_fix_objc_setExceptionMatcher(objc_exception_matcher fn)
{
// mtt_origin_hook_exception_matcher=fn;
if(!mtt_origin_hook_exception_matcher)
mtt_origin_hook_exception_matcher=fn;//赋予默认值,虽然这里也没意义...
if(mtt_origin_objc_setExceptionMatcher)
{
return mtt_origin_objc_setExceptionMatcher(mtt_hook_objc_exception_matcher);
}
return fn;
}
//objc_exception_try_exit
__attribute__((constructor)) static void mtt_fix_catch_exception()
{
struct rebinding catch_rebinding = {"objc_setExceptionMatcher", mtt_fix_objc_setExceptionMatcher, (void *)&mtt_origin_objc_setExceptionMatcher};
rebind_symbols((struct rebinding[1]){catch_rebinding}, 1);
//这里有个objc_setExceptionMatcher函数会set后返回默认的exception_matcher,这里hook一下,就能获取默认的exception matcher做一下交互,然后后续crash了就直接调这个exception_matcher即可.
//这样能hook整个app的所有@catch里的逻辑了.
mtt_origin_hook_exception_matcher=objc_setExceptionMatcher(mtt_hook_objc_exception_matcher);
}
五、总结
只有建立的监控上报后,才能知道,原来有那么多SDK,那么多业务或开发同学,如此喜欢用@try@catch去解决所谓的crash,你能想象几分钟内,发生了上百次try catch吗,这也太不对代码负责了啊。