关于android:Android-Crash-前的最后抢救

33次阅读

共计 3849 个字符,预计需要花费 10 分钟才能阅读完成。

家喻户晓,当 Andoird 程序产生未捕捉的异样的时候,程序会间接 Crash 退出。而所谓安全气囊,是指在 Crash 产生时捕捉异样,而后触发兜底逻辑,在程序退出前做最初的抢救。

一,Java 捕捉异样

在实现安全气囊之前,咱们先思考一个问题,像 bugly、sentry 这种监控框架是如何捕捉异样并上传堆栈的呢?要理解这个问题,咱们首先要理解一下当异样产生时是怎么流传的。

能够看到,异样到奔溃的流程很简略,次要分为以下几步:

  • 当抛出异样时,通过 Thread.dispatchUncaughtException 进行散发。
  • 顺次由 Thread,ThreadGroup,Thread.getDefaultUncaughtExceptionHandler 解决。
  • 在默认状况下,KillApplicationHandler 会被设置 defaultUncaughtExceptionHandler。
  • 而后 KillApplicationHandler 中会调用 Process.killProcess 退出利用。

能够看出,如果咱们通过 Thread.setDefaultUncaughtExceptionHandler 设置自定义处理器,就能够捕捉异样做一些兜底操作了,其实 bugly 这些库也是这么做的。

二、 自定义异样处理器

那么如果咱们设置了自定义处理器,在外面只做一些打印日志的操作,而不是退出利用,是不是就能够让 app 永不解体了呢?答案当然是否定的,次要有以下两个问题:

2.1 Looper 循环问题

咱们晓得,App 的运行在很大程序上依赖于 Handler 音讯机制,Handler 一直的往 MessageQueue 中发送 Message,而 Looper 则死循环的一直从 MessageQueue 中取出 Message 并生产,整个 app 能力运行起来。而当异样产生时,Looper.loop 循环被退出了,事件也就不会被生产了,因而尽管 app 不会间接退出,但也会因为无响应产生 ANR。因而,当解体产生在主线程时,咱们须要复原一下 Looper.loop。

2.2 主流程抛出异样问题

当咱们在主淤积抛出异样时,比方在 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)
    }
}

通过下面的步骤,咱们实现了一个 Java 层安全气囊,然而如果产生 Native 层解体时,程序还是会解体。那么咱们能不能依照 Java 层安全气囊的思路,实现一个 Native 层的安全气囊。

 

四、Native 层安全气囊

咱们晓得,Android Native 层异样是通过信号机制实现的。

  1. 当 crash 产生后,会在用户态阶段调用中断进入内核态。
  2. 在解决完内核操作,返回用户态时,会查看信号队列上是否有信号须要解决。
  3. 如果有信号须要解决,则会调用 sigaction 函数进行相应解决。

此时,咱们通过注册信号处理函数 sigaction 设置自定义信号处理器,即可实现 Native 的安全气囊。

须要留神的是,咱们能够通过 sigaction 设置自定义信号处理器,然而 SIGKILL 与 SIGSTOP 信号咱们是无奈更改其默认行为的,如果咱们设置了自定义信号处理器,没有退出 app,但谬误理论还是产生了,当谬误切实不可控时,零碎还是会发送 SIGKILL/SIGSTOP 信号,这个时候还会导致咱们 crash 时无奈获取真正的堆栈,因而咱们在自定义信号处理器时须要谨慎。能够看出,要理解 Native 异样捕捉,须要对 Linux 信号机制有肯定理解。

五、Native 层实现

在理解了 Native 层异样解决的原理之后,咱们通过自定义信号处理器来实现一个 Native 层的安全气囊,次要分为以下几步:

  1. 注册自定义信号处理器。
  2. 获取 Native 堆栈并与配置堆栈进行比拟。
  3. 如果匹配上了则疏忽相干解体,如果未匹配上则交给原信号处理器解决。

上面是 Native 层的代码实现:

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 写的 https://github.com/TestPlanB/mooner,反对捕捉 Android 基于“pthread_create”产生的子线程中异样业务逻辑产生信号,导致的 native crash。

参考代码:https://github.com/RicardoJiang/android-performance

正文完
 0