捕获代码里的@try@catch

一、目的

发现很多同学处理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吗,这也太不对代码负责了啊。

你可能感兴趣的:(捕获代码里的@try@catch)