前语

咱们都知道,当 Andoird 程序发生未捕获的反常的时分,程序会直接 Crash 退出

而所谓安全气囊,是指在 Crash 发生时,能够捕获反常,触发兜底逻辑,在程序退出前做最终的抢救

接下来咱们来看一下怎样完成一个安全气囊,以在 Crash 发生时做最终的抢救

Java 层安全气囊

Java 反常怎么捕获

在完成安全气囊之前,咱们先思考一个问题,像 bugly, sentry 这种库,是怎么捕获反常并上传仓库的呢?

要了解这个问题,咱们首先要了解一下当反常发生时是怎样传播的

【稳定性优化】安全气囊如何实现?

其实也很简单,首要分为以下几步

  1. 当抛出反常时,经过Thread.dispatchUncaughtException进行分发
  2. 依次由ThreadThreadGroupThread.getDefaultUncaughtExceptionHandler处理
  3. 在默许状况下,KillApplicationHandler会被设置defaultUncaughtExceptionHandler
  4. KillApplicationHandler中会调用Process.killProcess退出使用

这就是反常发生时的传播路径,能够看出,假如咱们经过Thread.setDefaultUncaughtExceptionHandler设置自定义处理器,就能够捕获反常做一些兜底操作了,其实 bugly 这些库也是这么做的

自定义反常处理器的问题

那么问题来了,假如咱们设置了自定义处理器,在里面只做一些打印日志的操作,而不是退出使用,是不是就能够让 app 永不溃散了呢?

答案当然是否定的,首要有以下两个问题

Looper 循环问题

【稳定性优化】安全气囊如何实现?

咱们知道,App 的运行在很大程序上依赖于 Handler 消息机制,Handler 不断的往 MessageQueue 中发送 Message,而Looper则死循环的不断从MessageQueue中取出Message并消费,整个 app 才能运行起来

而当反常发生时,Looper.loop 循环被退出了,事件也就不会被消费了,因而尽管 app 不会直接退出,但也会由于无响应发生 ANR

因而,当溃散发生在主线程时,咱们需求康复一下Looper.loop

主流程抛出反常问题

当咱们在主淤积抛出反常时,比方在onCreate办法中,尽管咱们捕获住了反常,但程序的执行也被中断了,界面的绘制或许无法完成,点击事件的设置也没有收效

这就导致了 app 尽管没有退出,但用户却无法操作的问题,这种状况好像还不如直接 Crash 了呢

因而咱们的安全气囊应该支持装备,只处理那些非主流程的操作,比方点击按钮触发的溃散,或者一些打点等对用户无感知操作形成的溃散

计划设计

为了解决上面提到的两个问题,咱们的计划如下

【稳定性优化】安全气囊如何实现?

首要分为以下几步:

  1. 注册自定义DefaultUncaughtExceptionHandler
  2. 当反常发生是捕获反常
  3. 匹配反常仓库是否契合装备,假如契合则捕获,不然交给默许处理器处理
  4. 判断反常发生时是否是主线程,假如是则重启Looper

代码完成

代码完成如下:

    fun setUpJavaAirBag(configList: List<JavaAirBagConfig>) {
        val preDefaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler()
        // 设置自定义处理器
        Thread.setDefaultUncaughtExceptionHandler { thread, exception ->
            handleException(preDefaultExceptionHandler, configList, thread, exception)
            if (thread == Looper.getMainLooper().thread) {
            	// 重启 Looper
                while (true) {
                    try {
                        Looper.loop()
                    } catch (e: Throwable) {
                        handleException(
                            preDefaultExceptionHandler, configList, Thread.currentThread(), e
                        )
                    }
                }
            }
        }
    }
    private fun handleException(
        preDefaultExceptionHandler: Thread.UncaughtExceptionHandler,
        configList: List<JavaAirBagConfig>,
        thread: Thread,
        exception: Throwable
    ) {
    	// 匹配装备
        if (configList.any { isStackTraceMatching(exception, it) }) {
            Log.w("StabilityOptimize", "Java Crash 已捕获")
        } else {
            Log.w("StabilityOptimize", "Java Crash 未捕获,交给原有 ExceptionHandler 处理")
            preDefaultExceptionHandler.uncaughtException(thread, exception)
        }
    }

Native 层安全气囊

经过上面的过程,咱们完成了一个 Java 层安全气囊,可是假如发生 Native 层溃散时,程序仍是会溃散

那么咱们能不能依照 Java 层安全气囊的思路,完成一个 Native 层的安全气囊?

Native 反常怎么捕获

Native 层反常是经过信号机制完成的

【稳定性优化】安全气囊如何实现?

  1. crash发生后,会在用户态阶段调用中断进入内核态
  2. 在处理完内核操作,返回用户态时,会检查信号队列上是否有信号需求处理
  3. 假如有信号需求处理,则会调用sigaction函数进行相应处理

那么假如咱们经过注册信号处理函数sigaction设置自定义信号处理器,是不是能够完成跟 Java 安全气囊相同的作用?

需求注意的是,咱们能够经过sigaction设置自定义信号处理器,可是SIGKILLSIGSTOP信号咱们是无法更改其默许行为的,假如咱们设置了自定义信号处理器,没有退出 app,但错误实践仍是发生了,当错误真实不可控时,系统仍是会发送SIGKILL/SIGSTOP信号,这个时分还会导致咱们 crash 时无法获取真实的仓库,因而咱们在自定义信号处理器时需求稳重

能够看出,要了解 Native 反常捕获,需求对 Linux 信号机制有一定了解,想了解更多的同学能够查看:写给android开发的Linux 信号 – 上篇

代码完成

在了解了 Native 层反常处理的原理之后,咱们经过自定义信号处理器来完成一个 Native 层的安全气囊,首要分为以下几步

  1. 注册自定义信号处理器
  2. 获取 Native 仓库并与装备仓库进行比较
  3. 假如匹配上了则疏忽相关溃散,假如未匹配上则交给原信号处理器处理
extern "C" JNIEXPORT void JNICALL
Java_com_zj_android_stability_optimize_StabilityNativeLib_openNativeAirBag(
        JNIEnv *env,
        jobject /* this */,
        jint signal,
        jstring soName,
        jstring backtrace) {
    do {
        //...
        struct sigaction sigc;
        // 自定义处理器
        sigc.sa_sigaction = sig_handler;
        sigemptyset(&sigc.sa_mask);
        sigc.sa_flags = SA_SIGINFO | SA_ONSTACK | SA_RESTART;
        // 注册信号
        int flag = sigaction(signal, &sigc, &old);
    } while (false);
}
static void sig_handler(int sig, struct siginfo *info, void *ptr) {
	// 获取仓库
    auto stackTrace = getStackTraceWhenCrash();
    // 与装备的仓库进行匹配
    if (sig == airBagConfig.signal &&
        stackTrace.find(airBagConfig.soName) != std::string::npos &&
        stackTrace.find(airBagConfig.backtrace) != std::string::npos) {
        LOG("反常信号已捕获");
    } else {
    	// 没匹配上的交给原有处理器处理
        LOG("反常信号交给原有信号处理器处理");
        sigaction(sig, &old, nullptr);
        raise(sig);
    }
}

存在的问题

经过上面的过程,其实 Native 层的安全气囊现已完成了,在 demo 中触发 Native Crash 能够被捕获到

可是信号处理函数有必要是async-signal-safe和可重入的,理论上不应该在信号处理函数中做太多作业,比方malloc等函数都不是可重入的

而咱们在信号处理函数中获取了仓库,打印了日志,很或许会形成一些意料之外的问题

理论上咱们能够在子线程获取仓库,在信号处理函数中只需求发出信号就能够了,但我尝试在子线程中使用 unwind 获取仓库,发现获取不到真实的仓库,因而还存在一定的问题,有了解的大佬能够在谈论区点拨下

Native 层安全气囊的计划也能够看看@Pika 写的github.com/TestPlanB/m…,支持捕获 Android 根据“pthread_create” 发生的子线程中反常事务逻辑发生信号,导致的native crash

总结

本文首要介绍了Java 层与 Native 层安全气囊的完成计划与反常捕获原理,在一些非主流程的 Crash 发生时,经过安全气囊能够做一些最终的抢救,在降低溃散率方面应该仍是有一些使用场景的,期望本文对你有所帮助~

示例代码

本文一切源码可见:github.com/RicardoJian…