Android 7.1 Toast修复之终极篇,进程不奔溃(包含apk和兼容外来dex插件)

news2024/12/29 1:42:22

修复android 7.1 Toast的篇章

  • 常规app通过ams lancet 字节编码处理:Android Lancet Aop 字节编码修复7.1系统Toast问题(WindowManager$BadTokenException)
  • 多渠道游戏app兼容性处理:Android 7.1 Toast修复之多渠道包动态使用Booster或者Lancet plugin

以上方式可以处理掉百分之90%问题,确保编译出apk 不存在toast 问题;

但有些sdk(特别是广告业务等) 是会加载外部插件(如:assets或者服务器上dex), 这种情况下在android 7.1的设备上发生异常是无法处理的。在Bugly上的crash数量,也是无法完全被消灭,领导也是持续关注,压力山大;

思路分析

插件是app 进程运行时,动态加载进去的,当插件中toast 发生crash 时,会抛出异常。那有没有方式可以在运行时,捕获到该异常?

经过查找资料,发现Java异常都是可以通过Thread.UncaughtExceptionHandler捕捉的。

场景模拟验证:
通过设置自定义UncaughtExceptionHandler子类去捕获,结合手抛BadTokenException模拟场景,发现可以捕获该异常,但主线程结束了,进程被关闭了。

继续猜想,有没有方式,当主线程发生java 异常继续执行?

查找ActivityThread(即主线程)的消息处理机制,发现Looper.loop() 是让主线程一直执行任务的关键;

  public static void main(String[] args) {
      Looper.prepareMainLooper();
      if (sMainThreadHandler == null) {
        sMainThreadHandler = thread.getHandler();
      }
      Looper.loop();
 }

根据这个思路,当进程中的toast 发生异常时,通过异常handler处理器捕捉该异常,接着继续调用looper.loop() 让主线程恢复执行,就完美解决该问题了。

编码

先编写Toast 异常筛选的代码:

    @Override
    public void uncaughtException(Thread t, Throwable e) {
        Log.i(TAG, "uncaughtException ");
        if (interruptException(t, e)) {
            // 让主线程,继续恢复消息处理
            resumeMainThreadLoop();
            return;
        }
        //一定要传递: 其他异常,继续分发给其他异常处理器(比如Bugly等)
        if (mOldHandler != null) {
            mOldHandler.uncaughtException(t, e);
        }
    }
   /**
     * 匹配toast的BadTokenException:
     * 1.匹配主线程上调用;
     * 2.匹配BadTokenException异常;
     * 3.Toast$TN 的调用栈
     *
     * @param t
     * @param e
     * @return
     */
    private boolean interruptException(Thread t, Throwable e) {
        if (t == null || e == null) {
            return false;
        }
        if (e instanceof WindowManager.BadTokenException && t.getName().contains("main")) {
            boolean match_toast = false;
            try {
                //获取到该异常的调用栈
                StackTraceElement[] elements = e.getStackTrace();
                if (elements != null) {
                    for (StackTraceElement element : elements) {
                        //匹配调用栈中该类的名字
                        if (element.getClassName().contains("Toast")) {
                            match_toast = true;
                            break;
                        }
                    }
                }
            } catch (Exception exception) {
                match_toast = true;
            }
            Log.i(TAG, "interrupt BadTokenException: " + t.toString() + " , " + e.toString() + (mOldHandler != null ? mOldHandler.toString() : "null oldHandler"));
            return match_toast;
        }
        return false;
    }    

这里需要注意点:所有的BadTokenException 并不是Toast一个因素触发的,咱们需要处理的是Toast 发生的。因此需要匹配三个点:

  • 1.匹配主线程上调用;
  • 2.匹配BadTokenException异常;
  • 3.Toast$TN 的调用栈,如下所示:
    在这里插入图片描述

接着编写主线程中恢复消息处理的代码,如下:

    @Override
    public void uncaughtException(Thread t, Throwable e) {
        Log.i(TAG, "uncaughtException ");
        if (interruptException(t, e)) {
            resumeMainThreadLoop();
            return;
        }
        // 分发给其他异常处理器,比如bugly等等
        if (mOldHandler != null) {
            mOldHandler.uncaughtException(t, e);
        }
    }
    private void resumeMainThreadLoop() {
        try {
            Log.i(TAG, "looper " + Looper.myLooper());

            if (Looper.myLooper() == null) {
                Looper.prepare();
            }
            Looper.loop(); // 当执行异常的时候,需要被捕捉该异常,分发给处理者
        } catch (Exception e) {
            /**
             * 注意点:
             *若是主线程中继续执行的任务期间,有异常发生,looper循环任务将结束。
             *因此,这里递归调用:不会死循环,也不会anr。
             *
             *这里递归调用是为了:若是需要消费的异常,则继续恢复looper.loop();
             *反之,则将异常上报给bugly之类的crash模块,结束进程。
             */
            uncaughtException(Thread.currentThread(), e);
        }
    }

注意点: 当 resumeMainThreadLoop()中looper.loop()发生异常时,并不会通知到UncaughtExceptionHandler#uncaughtException()中,需要手动通知其他异常处理器(即mOldHandler),因此递归调用uncaughtException(), 小伙伴不会担心会anr ,更不会是死循环。

异常处理器是如何分发异常的:
异常处理器UncaughtExceptionHandler可能会有许多个,最先设置的,最后调用。即线程发生异常时,会通知最后一个UncaughtExceptionHandler,向上递归调用到最先的UncaughtExceptionHandler。

接着写模拟手抛异常的代码,比较简单,省略。

最后考虑到使用范围,编写初始化方法:

    /**
     * 处理7.1 x的toast 问题,
     * 建议放到bugly 之后,用于防止上报被拦截的异常;
     */
    public static void init(boolean test) {
        if (init.compareAndSet(false, true)) {
            mainThread.postDelayed(() -> {
                // 小于或者等于7.1才开启
                boolean open = Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1;
                if (open) {
                    //一定要获取当前的异常处理器,用于分发异常。
                    Thread.UncaughtExceptionHandler mOldHandler = Thread.getDefaultUncaughtExceptionHandler();
                    if (!(mOldHandler instanceof ToastExceptionHandler)) {
                        Log.i(TAG, "proxy exception handler");
                        Thread.setDefaultUncaughtExceptionHandler(new ToastExceptionHandler(mOldHandler));
                    }
                }
            }, 1000L);
        }
    }

完整代码 , 如下所示:

package com.xingen.test.lancetlib;

import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.WindowManager;

import java.util.concurrent.atomic.AtomicBoolean;

/**
 * @author : HeXinGen
 * @date : 2023/4/12
 * @description :
 * <p>
 * 存在外部dex 插件,通过ams hook , 无法百分之百处理7.1 toast问题
 * 思路:
 * 通过Looper兜底的机制能够做到吃掉所有的java异常
 * <p>
 * 借鉴思路:https://www.infoq.cn/article/f6irpfwgcdc0rt54cx5z
 */
public class ToastExceptionHandler implements Thread.UncaughtExceptionHandler {
    private final Thread.UncaughtExceptionHandler mOldHandler;
    public static final String TAG = "ToastExceptionHandler";
    private static final Handler mainThread = new Handler(Looper.getMainLooper());

    public ToastExceptionHandler(Thread.UncaughtExceptionHandler mOldHandler) {
        this.mOldHandler = mOldHandler;
    }

    public static class ErrorMonitor {
        /**
         * 模拟手抛BadTokenException
         */
        public static void monitorBadTokenException() {
            String tip = "模拟7.1 toast error";
            Log.i(TAG, tip);
            throw new WindowManager.BadTokenException("模拟7.1 toast error");
        }

        /**
         * 用于测试上报bugly ,防止造成影响
         */
        public static void testBuglyReport() {
            String tip = "test bugly catch crash";
            Log.i(TAG, tip);
            throw new RuntimeException(tip);
        }
    }

    private static AtomicBoolean init = new AtomicBoolean(false);

    /**
     * 处理7.1 x的toast 问题,
     * 建议放到bugly 之后,用于防止上报被拦截的异常;
     */
    public static void init(boolean test) {
        if (init.compareAndSet(false, true)) {
            mainThread.postDelayed(() -> {
                // 小于或者等于7.1才开启
                boolean open = Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1;
                if (open) {
                    Thread.UncaughtExceptionHandler mOldHandler = Thread.getDefaultUncaughtExceptionHandler();
                    if (!(mOldHandler instanceof ToastExceptionHandler)) {
                        Log.i(TAG, "proxy exception handler");
                        Thread.setDefaultUncaughtExceptionHandler(new ToastExceptionHandler(mOldHandler));
                    }
                }
            }, 1000L);
        }
    }


    @Override
    public void uncaughtException(Thread t, Throwable e) {
        Log.i(TAG, "uncaughtException ");
        if (interruptException(t, e)) {
            resumeMainThreadLoop();
            return;
        }
        if (mOldHandler != null) {
            mOldHandler.uncaughtException(t, e);
        }
    }

    /**
     * 让当前线程继续运行
     * <p>
     * 借鉴ActivityThread:
     * public static void main(String[] args) {
     * Looper.prepareMainLooper();
     * <p>
     * if (sMainThreadHandler == null) {
     * sMainThreadHandler = thread.getHandler();
     * }
     * Looper.loop();
     * }
     */
    private void resumeMainThreadLoop() {
        try {
            Log.i(TAG, "looper " + Looper.myLooper());

            if (Looper.myLooper() == null) {
                Looper.prepare();
            }
            Looper.loop(); // 当执行异常的时候,需要被捕捉该异常,分发给处理者
        } catch (Exception e) {
            /**
             * 注意点:
             *若是主线程中继续执行的任务期间,有异常发生,looper循环任务将结束。
             *因此,这里递归调用:不会死循环,也不会anr。
             *
             *这里递归调用是为了:若是需要消费的异常,则继续恢复looper.loop();
             *反之,则将异常上报给bugly之类的crash模块,结束进程。
             */
            uncaughtException(Thread.currentThread(), e);
        }
    }

    /**
     * 匹配toast的BadTokenException:
     * 1.匹配主线程上调用;
     * 2.匹配BadTokenException异常;
     * 3.Toast$TN 的调用栈
     *
     * @param t
     * @param e
     * @return
     */
    private boolean interruptException(Thread t, Throwable e) {
        if (t == null || e == null) {
            return false;
        }
        if (e instanceof WindowManager.BadTokenException && t.getName().contains("main")) {
            boolean match_toast = false;
            try {
                StackTraceElement[] elements = e.getStackTrace();
                if (elements != null) {
                    for (StackTraceElement element : elements) {
                        //匹配调用栈
                        if (element.getClassName().contains("Toast")) {
                            match_toast = true;
                            break;
                        }
                    }
                }
            } catch (Exception exception) {
                match_toast = true;
            }
            Log.i(TAG, "interrupt BadTokenException: " + t.toString() + " , " + e.toString() + (mOldHandler != null ? mOldHandler.toString() : "null oldHandler"));
            return match_toast;
        }
        return false;
    }

}

验证结果

手抛toast异常验证:
在这里插入图片描述

抛出其他异常,验证其他异常处理器继续功能(bugly上报)无问题:

在这里插入图片描述
查看bugly上的记录:
在这里插入图片描述
没有记录手抛Toast异常的记录,但记录了恢复主线程消息机制后的异常,证明可行性。

延伸点

这个方案并不只适合Toast异常,也可以适用于其他一些不影响主流程的异常或者非核心页面的奔溃等等,也可以结合服务器来做到动态下发异常拦截,更加灵活性;当然有些异常必须杀死进程,涉及金钱的异常等等;

借鉴:

  • 有赞团队对异常处理方式

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/421768.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

在外web浏览器远程访问jupyter notebook服务器【内网穿透】

文章目录前言视频教程1. Python环境安装2. Jupyter 安装3. 启动Jupyter Notebook4. 远程访问4.1 安装配置cpolar内网穿透4.2 创建隧道映射本地端口转载自远控源码文章&#xff1a;公网远程访问jupyter notebook【cpolar内网穿透】 前言 Jupyter Notebook&#xff0c;它是一个交…

未来城市的微小单元:滴滴即将量产无人车

汽车诞生之后就一直作为除了家庭与公司之外的「第三空间」存在&#xff0c;技术的脚步从未停止过开发汽车的更多可能。尤其无人驾驶技术的出现&#xff0c;进一步解放了驾驶者&#xff0c;也让人们对于这一能够自主移动的第三空间充满了想象。作为未来城市的微小组成单元&#…

( “树” 之 DFS) 226. 翻转二叉树 ——【Leetcode每日一题】

226. 翻转二叉树 给你一棵二叉树的根节点 root &#xff0c;翻转这棵二叉树&#xff0c;并返回其根节点。 示例 1&#xff1a; 输入&#xff1a;root [4,2,7,1,3,6,9] 输出&#xff1a;[4,7,2,9,6,3,1] 示例 2&#xff1a; 输入&#xff1a;root [2,1,3] 输出&#xff1a;[…

ThreadLocal源码分析及内存泄漏

ThreadLocal原理分析及内存泄漏ThreadLocal的使用ThreadLocal原理set方法解析replaceStaleEntry方法解析expungeStaleEntry方法解析cleanSomeSlots方法解析case 1: 向前有脏数据&#xff0c;向后找到可覆盖的Entrycase 2: 向前有脏数据&#xff0c;向后未找到可覆盖的Entrycase…

吴恩达机器学习--线性回归

文章目录前言一、单变量线性回归1.导入必要的库2.读取数据3.绘制散点图4.划分数据5.定义模型函数6.定义损失函数7.求权重向量w7.1 梯度下降函数7.2 最小二乘法8.训练模型9.绘制预测曲线10.试试正则化11.绘制预测曲线12.试试sklearn库二、多变量线性回归1.导入库2.读取数据3.划分…

掌握高效绘制地图的利器——LeafletJs

文章目录前言一、leafletJs是什么&#xff1f;二、快速入门1、安装2、快速入门三、进阶学习1、Map 控件2、Marker 标记3、Popup 弹出窗口4、图层四、项目实战封装文件4.1 基础点位图4.2 行驶轨迹图前言 GIS 作为获取、存储、分析和管理地理空间数据的重要工具&#xff0c;用 G…

数据结构与算法一览(树、图、排序算法、搜索算法等)- Review

算法基础简介 - OI Wiki (oi-wiki.org) 文章目录1. 数据结构介绍1.1 什么是数据结构1.2 数据结构分类2. 链表、栈、队列&#xff1a;略3. 哈希表&#xff1a;略4. 树4.1 二叉树4.2 B 树与 B 树4.3 哈夫曼&#xff08;霍夫曼&#xff09;树&#xff1a;Huffman Tree4.4 线段树&a…

编辑文件/文件夹权限 - Win系统

前言 我们经常会遇到由于权限不够无法删除文件/文件夹的情况&#xff0c;解决方案一般是编辑文件/文件夹的权限&#xff0c;使当前账户拥有文件的完全控制权限&#xff0c;然后再进行删除&#xff0c;下文介绍操作步骤。 修改权限 查看用户权限 右键文件/文件夹&#xff0c;…

(函数指针) 指向函数的指针

函数指针- 指向函数的指针函数指针的声明和使用通过函数指针调用函数函数指针做参数函数指针数组函数指针的声明和使用 函数指针的声明格式&#xff1a; 返回值类型 (*函数指针名)(参数列表); 其中&#xff1a; *函数指针名 表示函数指针的名称返回值类型 则表示该指针所指向…

【Kubernetes】StatefulSet对象详解

文章目录简介1. StatefulSet对象的概述、作用及优点1.1 对比Deployment对象和StatefulSet对象1.2 以下是比较Deployment对象和StatefulSet对象的优缺点&#xff1a;2. StatefulSet对象的基础知识2.1 StatefulSet对象的定义2.1.1 下表为StatefulSet对象的定义及其属性&#xff1…

上岸川大网安院

一些感慨 一年多没写过啥玩意了&#xff0c;因为考研去了嘿嘿。拟录取名单已出&#xff0c;经历一年多的考研之路也可以顺利打上句号了。 我的初试成绩是380&#xff0c;政治65&#xff0c;英语81&#xff0c;数学119&#xff0c;专业课115。 回顾这一路&#xff0c;考研似乎也…

分类预测 | MATLAB实现CNN-BiLSTM-Attention多输入分类预测

分类预测 | MATLAB实现CNN-BiLSTM-Attention多输入分类预测 目录分类预测 | MATLAB实现CNN-BiLSTM-Attention多输入分类预测分类效果基本介绍模型描述程序设计参考资料分类效果 基本介绍 MATLAB实现CNN-BiLSTM-Attention多输入分类预测&#xff0c;CNN-BiLSTM结合注意力机制多输…

Vue3使用Vant组件库避坑总结

文章目录前言一、问题二、解决方法三、问题出现原因总结经验教训前言 本片文章主要写了&#xff0c;Vue3开发时运用Vant UI库的一些避坑点。让有问题的小伙伴可以快速了解是为什么。也是给自己做一个记录。 一、问题 vue3版本使用vant失败&#xff0c;具体是在使用组件时失效…

IPBX系统快速部署和Freeswitch 1.10.7自动安装

IPBX系统部署文档 IPPBX系统 1.10.7版本Freeswitch &#xff0c;手机互联互通&#xff0c;SIP协议&#xff0c;分机互相拨打免费通话清晰&#xff0c;支持wifi或4G网络互相拨打电话&#xff0c;可以对接OLT设备&#xff0c;系统可以部署到本地物理机&#xff0c;也可以部署到阿…

工程质量之研发过程管理需要关注的点

一、背景 作为程序猿&#xff0c;工程质量是我们逃不开的一个话题&#xff0c;工程质量高带来的好处多多&#xff0c;我在写这篇文章的时候问了一下CHATGPT&#xff0c;就当娱乐一下&#xff0c;以下是ChatGPT的回答&#xff1a; 1、提高产品或服务的可靠性和稳定性。高质量的系…

光时域反射仪那个品牌的好用

光时域反射仪 哪个品牌好用 光时域反射仪要怎么选到合适自己的&#xff0c;这些问题 可能一直在困扰这一线的工作人员&#xff0c;下面小编就为大家一一解答下 首先光时域域反射仪是一款检测光纤线路的损耗 长度 以及 事件点的一款设备&#xff0c;在诊断 光纤线路 故障点的情…

从零开始学架构——CAP理论

CAP定理 CAP 定理&#xff08;CAP theorem&#xff09;又被称作布鲁尔定理&#xff08;Brewer’s theorem&#xff09;&#xff0c;是加州大学伯克利分校的计算机科学家埃里克布鲁尔&#xff08;Eric Brewer&#xff09;在 2000 年的 ACM PODC 上提出的一个猜想。2002 年&…

Web前端 HTML、CSS

HTML与CSSHTML、CSS思维导图一、HTML1.1、HTML基础文本标签1.2、图片、音频、视频标签1.3、超链接、表格标签1.4、布局1.5、表单标签1.6、表单项标签综合使用1.7、HTML小结二、CSS&#xff08;简介&#xff09;2.1、引入方式2.2、选择器2.3、CSS属性Web前端开发总览 Html&…

案例拆解丨ChatGPT+塔罗牌,批量起号、暴利引流,小白也能轻松月入10000+

ChatGPT 的出现&#xff0c;大大拉低了很多行业的门槛&#xff0c;比如客服、教育、翻译、自媒体……而塔罗牌占卜&#xff0c;肯定也是其中之一。 塔罗牌是一种占卜工具&#xff0c;由78张牌组成。可以用于占卜、灵性探索、个人成长和自我发现。 这是一个相对小众&#xff0c…

LinuxGUI自动化测试框架搭建(十三)-创建工具集目录tools并封装文件复制方法cpoyFile.py

(十三)-创建工具集目录tools并封装文件复制方法cpoyFile.py 1 tools的作用2 创建tools目录3 创建文件复制方法cpoyFile.py4 设计cpoyFile.py4.1 安装shutil4.2 导入模块4.3 脚本设计5 目前框架目录1 tools的作用 为了存放框架需要用到的一些常用工具或方法,比如文件复制功能…