Android Crash 前的最后抢救

标签: android | 发表时间:2023-04-14 11:33 | 作者:xiangzhihong
出处:https://segmentfault.com/blogs

众所周知,当 Andoird 程序发生未捕获的异常的时候,程序会直接 Crash 退出。而所谓安全气囊,是指在 Crash 发生时捕获异常,然后触发兜底逻辑,在程序退出前做最后的抢救。

一,Java 捕获异常

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

image.png

可以看到,异常到奔溃的流程很简单,主要分为以下几步:

  • 当抛出异常时,通过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。

image.png

2.2 主流程抛出异常问题

当我们在主淤积抛出异常时,比如在onCreate方法中,虽然我们捕获住了异常,但程序的执行也被中断了,界面的绘制可能无法完成,点击事件的设置也没有生效。这就导致了 app 虽然没有退出,但用户却无法操作的问题,这种情况似乎还不如直接 Crash 了呢。

因此我们的安全气囊应该支持配置,只处理那些非主流程的操作,比如点击按钮触发的崩溃,或者一些打点等对用户无感知操作造成的崩溃。
 

三、方案设计

为了解决上面提到的两个问题,我们提出了如下的方案:

image.png

思路如下:

  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 层异常是通过信号机制实现的。
image.png

  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

相关 [android crash] 推荐:

Android程序Crash时的异常上报

- - CSDN博客移动开发推荐文章
转载请注明来源:http://blog.csdn.net/singwhatiwanna/article/details/17289479. 大家都知道,android应用不可避免的会发生crash,无论你的程序写的多完美,总是无法完全避免crash的发生,可能是由于android系统底层的bug,也可能是由于不充分的机型适配或者是糟糕的网络状况.

Android NDK开发Crash错误定位

- - 极客521 | 极客521
在Android开发中,程序Crash分三种情况:未捕获的异常、ANR(Application Not Responding)和闪退(NDK引发错误). 其中 未捕获的异常根据logcat打印的堆栈信息很容易定位错误. ANR错误也好查,Android规定,应用与用户进行交互时,如果5秒内没有响应用户的操作,则会引发ANR错误,并弹出一个系统提示框,让用户选择继续等待或立即关闭程序.

Android Crash 前的最后抢救

- - SegmentFault 最新的文章
众所周知,当 Andoird 程序发生未捕获的异常的时候,程序会直接 Crash 退出. 而所谓安全气囊,是指在 Crash 发生时捕获异常,然后触发兜底逻辑,在程序退出前做最后的抢救. 在实现安全气囊之前,我们先思考一个问题,像 bugly、sentry 这种监控框架是如何捕获异常并上传堆栈的呢.

android开发之应用Crash自动抓取Log_自动保存崩溃日志到本地

- - CSDN博客推荐文章
应用发生crash之后要查看log,判断问题出在什么地方,可是一旦应用发布出去,就要想办法把用户的崩溃日志拿到分析. 所以要在发生crash之后抓取log,然后上传到服务器,方便开发者查看,现在都有很多第三方做这方面的服务,这里说下如何自己来实现. 其实原理很简单,应用出现异常后,会由默认的异常处理器来处理异常,.

JVM Crash导致的重启

- - 企业架构 - ITeye博客
32操作系统下 发生jvm crash . 从表面看是由于OO导致的. 可以发现程序进程号为924,出现问题的线程号为:912. 可以尝试解决办法:-XX:-DoEscapeAnalysis. CPU:total 4 (4 cores per cpu, 1 threads per core) family 6 model 23 stepping 6, cmov, cx8, fxsr, mmx, sse, sse2, sse3, ssse3, sse4.1, tsc.

iOS crash 崩溃问题的追踪方法

- - 移动开发 - ITeye博客
在调试程序的时候,总是碰到crash的bug,而且一追踪就是一些汇编的代码,让人特别疑惑. 一般情况下可以通过增加两天断点来解决此问题,方法介绍如下:. 基本上有错误分为以下几种类型:. SIGABRT和EXC_BAD_ACCESS较为特殊,算是比较好跟进. SIGABRT是系统报错,在memery warning之后,系统会把程序强制退出,报的就是这个错误.

iOS应用的crash日志的分析基础

- - CSDN博客移动开发推荐文章
一、如何获得crash日志. 当一个iOS应用程序崩溃时,系统会创建一份crash日志保存在设备上. 这份crash日志记录着应用程序崩溃时的信息,通常包含着每个执行线程的栈调用信息(低内存闪退日志例外),对于开发人员定位问题很有帮助. 如果设备就在身边,可以连接设备,打开Xcode - Window - Organizer,在左侧面板中选择Device Logs(可以选择具体设备的Device Logs或者Library下所有设备的Device Logs),然后根据时间排序查看设备上的crash日志.

DB2如何进行crash recovery原理浅析(转载)

- - 数据库 - ITeye博客
          跟大家一样,我也很好奇DB2是如何做crash recovery的. 最近看了一点 dabase crash recovery的资料,我把我对数据库 recovery的理解贴出来跟大家讨论讨论,这样可以帮助大家更好的理解DB2的行为. 也希望实验室的专家们能够多给我们讲点这方面的知识.

java.util.zip.Deflater使用不当引发jvm crash及问题排查

- - Linux - 操作系统 - ITeye博客
最近使用第三方开源库jflvlib录制flv格式视频,测试过程发现,视频录制进程经常挂掉;. java启动参数中已经配置内存溢出时导出日志文件-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/export/Logs/xxx.dump和jvm crash的日志输出路径-XX:ErrorFile=/export/Logs/xxx.log,但是进程挂掉后没有找到任何日志输出;.

Tomcat启动:A fatal error has been detected by the Java Runtime Environment(JVM Crash分析及相关资料)

- - CSDN博客推荐文章
2013年1月10号注:. 今天更新代码之后,突然出现一个问题:Tomcat启动时,总是会出现jvm fatal error错误导致tomcat无法正常启动,以下是错误信息:. 在网上搜寻好久,终于找到了类似错误,网上解释归结于:JIT在做编译优化的时候处理时出错,可能是触发了JVM的编译器的BUG导致的.