转载

保护你的crash

保护你的crash

尽管APM有相当多的采集指标,但假如只能监控一种数据,那么必然会选择crash。毫不客气的说,crash在APM中绝对可以占据80%甚至更多的地位。因此,如果一旦crash采集的数据发生了异常,对于APM相关的开发人员来说,绝对是一个噩耗。

crash采集

细分之后,crash可以被分为mach exception、signal以及NSException三种类型,每一种类型有着对应的捕获方式。

mach exception mach exception通过端口的方式传递,在异常抛出之后会依次投递到thread、task和host端口。系统提供了一部分的端口API,包括创建和注册对应的exception handler。但即便我们注册了对应的处理程序,也不会干扰原有的投递流程,最终mach exception会经过一系列的转换成为signal信号。

保护你的crash 

NSException NSException对异常信息进行了抽象封装,面向对象的设计使得我们可以使用try-catch来避免应用异常崩溃,这是其他两种crash不具备的。和mach exception一样,NSException同样能被转换成SIGABRT抛出。另外,假如我们使用NSSetUncaughtExceptionHandler去注册回调处理,那么对应的crash不会转换成signal

signal signal是一种强大的机制,异常处理仅仅是它一部分的功能。signal.h文件中声明了32种异常信号,一般crash需要捕获的为6-8种。对于每一种signal异常,都能在网上找到具体的异常信息,这里不再多说。由于mach exception和NSException最终都能被转换成signal,因此理论上我们只需要注册signal的处理就可以了

保护你的crash

由于crash的捕获机制只允许我们注册一个回调函数,因此多个crash采集框架很可能会存在冲突。为了避免冲突导致回调流程失效,注册前都应该检测是否存在已注册的handler,保证多个handler可以像响应链一般连续的执行下去。以信号注册sigaction函数为例,正确的注册代码如下:

static struct sigaction registered_action;
struct sigaction my_action;
void signal_handler(int signal) {
    ......
}
myAction.sa_handler = &signal_handler;
sigemptyset(&my_action.sa_mask);
sigaction(SIGABRT, &my_action, ®istered_action);

但是,即便我们对冲突做了防范处理,并不代表第三方框架在我们注册之后会同样善待我们的handler。某年某月,线上的crash上报数量堪称飞流直下三千尺。经过了一系列的排查,最终发现问题出在第三方注册了多个与我们相同的signal回调并且没有做冲突处理。

冲突解决方案

解决问题应当寻求代价最小的方案,并且要避免相同的问题今后发生之后也能有效的解决。另外,除了要保护我们自己的crash回调之外,由于回调的过程中存在二次crash以及人工abort的风险,我们应当保证我们的handler是第一个被回调的。首先除开load阶段发生crash的可能,为了减少遗漏,注册的时机应当是尽可能的提前。所以大部分框架选择注册crash的时机是在didLaunch方法里面。

保护你的crash

周期性检测

利用已有或者新建一个周期回调,去检测我们注册的handler是否被替换,然后做对应的处理。

监听应用状态

基本上在应用进入active状态之前,crash相关的注册都已经完成,我们可以接收对应的状态通知来保护我们注册的handler

    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
        ......
        [[NSNotificationCenter defaultCenter] addObserver: [SignalHandler sharedHandler] selector: @selector(checkRegisterCrashHandler) name: UIApplicationDidBecomeActiveNotification object: nil];
        ......   
    }
    
    static struct sigaction existActions[32];
    static int fatal_signals[] = {
        SIGILL,
        SIGBUS,
        SIGABRT,
        SIGPIPE,
    };
    
    - (void)checkRegisterCrashHandler {
        struct sigaction oldAction;
        for (int idx = 0; idx < sizeof(fatal_signals) / sizeof(int); idx++) {
            sigaction(fatal_signals[idx], NULL, &oldAction);
            if (oldAction.sa_handler != &signal_handler) {
                existActions[fatal_signals[idx]] = oldAction;
                
                struct sigaction myAction;
                myAction.sa_handler = &signal_handler;
                sigemptyset(&myAction.sa_mask);
                sigaction(SIGABRT, &myAction, NULL);
            }
        }
    }

定时器检测

创建定时器来控制检测周期,比起监听应用状态有更强的可控性。

    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
        ......
        NSTimer *timer = [[NSTimer alloc] initWithFireDate: [NSDate date] interval: 30 target: [SignalHandler sharedHandler] selector: @selector(checkRegisterCrashHandler) userInfo: nil repeats: YES];
        [[NSRunLoop currentRunLoop] addTimer: timer forMode: NSRunLoopCommonModes];
        [timer fire];
        ......   
    }

周期性检测方案存在的风险是,如果在两个周期之间发生了注册冲突,并且随后发生crash时,并不能保证我们的handler会被调起。周期性检测的方案优点在于简单,但是由于重复注册handler,可能导致回调时也被多次调用。

hook

虽然crash相关的捕获接口都是C语言编写的,但fishhook提供了对于C语言级别的hook方案。通过hook对应的捕获接口,检测注册的handler是否为我们的回调:

struct SignalHandler {
    void (*signal_handler)(int);
    struct SignalHandler *next;
}
struct SignalHandler *previousHandlers[32];
void append(struct SignalHandler *handlers, struct SignalHandler *node) { 
    ......
}
static int (*origin_sigaction)(int, const struct sigaction *__restrict, struct sigaction * __restrict) = NULL;
int custom_sigaction(int signal, const struct sigaction *__restrict new_action, struct sigaction * __restrict old_action) {
    if (new_action.sa_handler != signal_handler) {
        append(previousHandlers[signal], new_action);
        return origin_sigaction(signal, NULL, old_action);
    } else {
        return origin_sigaction(signal, new_action, old_action);
    }
}

如果检测到即将注册的handler不是我们的注册函数,那么将注册的回调添加到回调链表中,在我们处理完成之后逐一唤起调用。这种hook机制存在的一个风险是假如我们先进行了注册,后续其他的注册如果做了冲突处理。那么由于其他handler会保留已注册的回调函数,就是我们的signal_handler,将会导致crash发生后,我们的signa_handler将会被调起多次:

保护你的crash

保护你的crash

解决方案之一是永远不告诉其他的注册者已经存在的回调函数,避免发生二次回调:

int custom_sigaction(int signal, const struct sigaction *__restrict new_action, struct sigaction * __restrict old_action) {
    if (new_action.sa_handler != signal_handler) {
        append(previousHandlers[signal], new_action);
        return origin_sigaction(signal, NULL, NULL);
    } else {
        return origin_sigaction(signal, new_action, old_action);
    }
}
又或者在我们的signal_handler中采用单例方式写法,保证回调处理只被执行一次:
void signal_handler(int signal) {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        ......
    });
}

hook的方式对注册的回调函数有了更高的控制力,并且保证回调的顺序总是我们的回调在前。但是,如果其他框架中同样hook了相同的函数,做了过滤处理,可能将导致所有的handler都被过滤掉,最终所有的crash捕获都失效。

最简单的方式

保护你的crash

上述的方法都或多或少存在自己的不足,那么有没有不需要改动那么大,又可以避免发生采集冲突的做法呢?答案是不用冲突的SDK或者和对方协商提供一个不冲突的版本。

保护你的crash

尾言

踩坑是件很有趣的事情。踩坑让我们支付了时间、精力、甚至KPI,但是踩过的坑越多,我们才会越强大。

正文到此结束
Loading...