服务粉丝

我们一直在努力
当前位置:首页 > 财经 >

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

日期: 来源:程序员江同学收集编辑:程序员江同学

前言

我们都知道,当 Andoird 程序发生未捕获的异常的时候,程序会直接 Crash 退出

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

接下来我们来看一下怎么实现一个安全气囊,以在 Crash 发生时做最后的抢救

Java 层安全气囊

Java 异常如何捕获

在实现安全气囊之前,我们先思考一个问题,像 bugly, sentry 这种库,是如何捕获异常并上传堆栈的呢?

要了解这个问题,我们首先要了解一下当异常发生时是怎么传播的

p7.png

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

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

这就是异常发生时的传播路径,可以看出,如果我们通过Thread.setDefaultUncaughtExceptionHandler设置自定义处理器,就可以捕获异常做一些兜底操作了,其实 bugly 这些库也是这么做的

自定义异常处理器的问题

那么问题来了,如果我们设置了自定义处理器,在里面只做一些打印日志的操作,而不是退出应用,是不是就可以让 app 永不崩溃了呢?

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

Looper 循环问题

p8.png

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

而当异常发生时,Looper.loop 循环被退出了,事件也就不会被消费了,因此虽然 app 不会直接退出,但也会因为无响应发生 ANR

因此,当崩溃发生在主线程时,我们需要恢复一下Looper.loop

主流程抛出异常问题

当我们在主淤积抛出异常时,比如在onCreate方法中,虽然我们捕获住了异常,但程序的执行也被中断了,界面的绘制可能无法完成,点击事件的设置也没有生效

这就导致了 app 虽然没有退出,但用户却无法操作的问题,这种情况似乎还不如直接 Crash 了呢

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

方案设计

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

p9.jpg

主要分为以下几步:

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

p10.png
  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 写的https://github.com/TestPlanB/mooner,支持捕获 Android 基于“pthread_create” 产生的子线程中异常业务逻辑产生信号,导致的native crash

总结

本文主要介绍了Java 层与 Native 层安全气囊的实现方案与异常捕获原理,在一些非主流程的 Crash 发生时,通过安全气囊可以做一些最后的挽救,在降低崩溃率方面应该还是有一些应用场景的,希望本文对你有所帮助~

示例代码

本文所有源码可见:https://github.com/RicardoJiang/android-performance


相关阅读

  • Jetpack Compose 最新进展

  • 以下内容来自公众号code小生,关注每日干货及时送达原文链接 https://android-developers.googleblog.com/2022/05/whats-new-in-jetpack-compose.htmlJetpack Compose 1.0 发
  • 使用 Flutter 与 Firebase 制作 I/O 弹球游戏

  • 以下内容来自公众号code小生,关注每日干货及时送达文/ Very Good Ventures 团队,5 月 11 日发表于 Flutter 官方博客为了今年的 Google I/O 大会,Flutter 团队使用 Flutter 以
  • 微软正式宣布 Visual Studio 2022

  • ↓推荐关注↓转自:Rwingcnblogs.com/Rwing/p/visual-studio-2022.html首先,我们要感谢正在阅读这篇文章的你,我们所有的产品开发都始于你也止于你,无论你是在开发者社区上发帖,还
  • VSCode 自动化插件

  • 点击下方“逆锋起笔”,公众号回复 视频教程领取大佬们推荐的学习资料转载自:葡萄城技术团队来源:https://www.cnblogs.com/powertoolsteam/p/14609876.html编程是复杂的。在保
  • 太 仓 温 度 hot

  • 悠悠万事,民生为重政之所兴,在顺民心一点一滴皆用心一枝一叶总关情30年来,太仓持续把社会经济发展“大愿景”和改善民生“小目标”有效衔接,增进民生福祉,擦亮幸福底色。养老服务
  • 《HelloGitHub》第 84 期

  • 兴趣是最好的老师,HelloGitHub 让你对编程感兴趣!简介HelloGitHub 分享 GitHub 上有趣、入门级的开源项目。https://github.com/521xueweihan/HelloGitHub这里有实战项目、入
  • 10 款开源的在线游戏,点开就能玩的那种

  • 我早前写过一篇介绍 GitHub 上开源游戏的文章:《误入 GitHub 游戏区,结果意外地收获颇丰》,文中介绍了 5 款有趣、好玩的开源游戏,虽然当时那篇文章收获了广大读者的肯定,但也有
  • 十分钟搞懂限流及常见方案

  • 来源:blog.csdn.net/liuerchong/article/details/118882053文章目录限流基本概念QPS和连接数控制传输速率黑白名单分布式环境限流方案常用算法令牌桶算法漏桶算法滑动窗口常
  • 如何使用并查集解决朋友圈问题?

  • 这是 JsonChao 的第 347 期分享大家好,我是小彭。今天分享到的是一种相对冷门的数据结构 —— 并查集。虽然冷门,但是它背后体现的算法思想却非常精妙,在处理特定问题上能做到

热门文章

  • “复活”半年后 京东拍拍二手杀入公益事业

  • 京东拍拍二手“复活”半年后,杀入公益事业,试图让企业捐的赠品、家庭闲置品变成实实在在的“爱心”。 把“闲置品”变爱心 6月12日,“益心一益·守护梦想每一步”2018年四

最新文章

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

  • 前言我们都知道,当 Andoird 程序发生未捕获的异常的时候,程序会直接 Crash 退出而所谓安全气囊,是指在 Crash 发生时,可以捕获异常,触发兜底逻辑,在程序退出前做最后的抢救接下来
  • Jetpack Compose 最新进展

  • 以下内容来自公众号code小生,关注每日干货及时送达原文链接 https://android-developers.googleblog.com/2022/05/whats-new-in-jetpack-compose.htmlJetpack Compose 1.0 发
  • 全开源的 Android 手机或将到来

  • 以下内容来自公众号code小生,关注每日干货及时送达文 | Travis出品 | OSC开源社区(ID:oschina2013)近日,一位来自「Simple Mobile Tools」名为 "Tibbbi" 的开发者通过 Reddit
  • 张一鸣:给产品技术人才的建议

  • 点击下方“逆锋起笔”,公众号回复 视频教程领取大佬们推荐的学习资料作者:张一鸣排版:易总这是张一鸣多年前的一篇文章,彼时的头条还是个小公司,还在为招募人才发愁。这篇文章对
  • 薪酬最高的编程语言居然是?

  • 点击下方“逆锋起笔”,公众号回复 视频教程领取大佬们推荐的学习资料作者 | invozone译者 | 平川策划 | 凌敏要在软件开发领域有所发展,你需要用最新的编程技能武装自己。这就
  • 使用 Flutter 与 Firebase 制作 I/O 弹球游戏

  • 以下内容来自公众号code小生,关注每日干货及时送达文/ Very Good Ventures 团队,5 月 11 日发表于 Flutter 官方博客为了今年的 Google I/O 大会,Flutter 团队使用 Flutter 以