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