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

修复android 7.1 Toast的篇章

以上方式可以处理掉百分之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)) {
                // 小于或者等于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));
                    }
                }
        }
    }


    @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异常,也可以适用于其他一些不影响主流程的异常或者非核心页面的奔溃等等,也可以结合服务器来做到动态下发异常拦截,更加灵活性;当然有些异常必须杀死进程,涉及金钱的异常等等;

借鉴:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 您好!如果Android Studio中的Toast无法正常显示,可能是以下原因之一: 1. 您没有在Toast中设置要显示的文本。请确保在Toast的makeText()方法中传入要显示的文本: ``` Toast.makeText(getApplicationContext(), "要显示的文本", Toast.LENGTH_SHORT).show(); ``` 2. 您可能没有在正确的上下文中创建Toast。如果您在非Activity类中创建了Toast,那么您需要传入一个正确的上下文。例如,在Fragment中创建Toast,应该使用Fragment的getContext()方法: ``` Toast.makeText(getContext(), "要显示的文本", Toast.LENGTH_SHORT).show(); ``` 3. 您可能没有调用show()方法。请确保在创建Toast之后调用show()方法才能显示Toast。 如果以上方法仍然无法解决问题,您可以尝试清除项目缓存并重新构建项目,或者尝试在不同的设备或模拟器上运行应用程序 ### 回答2: 在Android开发中,Toast是一种简单的通知,用于在屏幕上显示一段文字或图标等。但是,有时候在使用Android Studio时,Toast可能会出现不显示的情况。下面是可能导致Toast不显示的几种原因以及相应的解决方法: 1. Toast没有被正确地初始化和显示。在初始化Toast时,需要通过调用makeText()方法来创建Toast对象,并在调用show()方法时显示Toast。如果忘记了调用show()方法,或者将show()方法和makeText()方法调用顺序错误,就会导致Toast不显示。可以检查代码中是否正确地调用了makeText()和show()方法。 2. Toast的显示时间太短。在调用show()方法时,可以设置Toast显示的持续时间。如果持续时间设置得太短,Toast可能会在显示出来之前就被关闭。可以尝试增加Toast的持续时间,或者将显示的文本内容缩短,以确保Toast能够被完整地显示出来。 3. Toast被其他布局元素遮盖。有时候,Toast可能会被其他布局元素遮盖,导致无法显示。可以通过将Toast的位置移动到屏幕上方或下方或改变布局元素的位置来解决这个问题。 4. Toast被不透明的背景遮盖。如果Toast的背景色与布局元素的背景颜色相同,或者Toast的背景色被设置为不透明,可能会导致Toast被遮盖而无法显示。可以尝试更改Toast的背景颜色或透明度,或者将Toast放置在一个不透明的布局元素之上以确保其能够显示出来。 总之,对于Toast不显示的问题,需要认真检查代码,确保Toast的初始化和显示方法调用正确无误,以及确保Toast没有被遮盖或设置不正确的背景色或透明度等问题。同时,注意Toast的显示时间和显示位置,以确保Toast能够被正确地显示出来。 ### 回答3: 在Android Studio中,Toast是一种用于显示简短信息的小型弹框。如果你遇到了Toast不显示的问题,可以尝试以下几种解决方法: 1. 检查Toast长度:Toast有两种长度:LENGTH_SHORT和LENGTH_LONG。如果你使用了LENGTH_SHORT,但是Toast还是不显示,你可以尝试使用LENGTH_LONG来调节Toast的长度。 2. 检查上下文:在调用Toast时,需要传入一个上下文参数。如果你传入的上下文参数不正确,Toast就无法显示。通常情况下,上下文参数应该是当前Activity或者当前Context。 3. 检查屏幕亮度:当屏幕亮度设置为0的时候,Toast会失效。你可以尝试将屏幕亮度调节到大于0的值来解决这个问题。 4. 检查主线程:在Android中,UI操作必须在主线程中执行。如果你在子线程中调用了ToastToast是无法显示的。你可以使用runOnUiThread()方法在主线程中调用Toast。 5. 检查背景色:有时候Toast会被应用程序的主题或者背景色所遮盖,导致无法显示。你可以尝试改变Toast的背景色,或者将Toast设置为透明来解决这个问题。 如果以上方法都无法解决问题,你可以尝试重新启动Android Studio或者重启设备。同时,你也可以在网络上查找相关解决方案或者咨询相关专业人士来解决问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值