Android Lancet Aop 字节编码修复7.1系统Toast问题(WindowManager$BadTokenException)
近期在Bugly上出现7.1以下设备上出现大量BadTokenException:
android.view.WindowManager$BadTokenException
Unable to add window -- token android.os.BinderProxy@6c0415d is not valid; is your activity running?
报错堆栈,如下所示:
1.定位分析:
查看Toast的源码可知,在android 7.1版本及其以下,没有对wm.addview()进行异常捕捉:
官方在android8.0 以上修复该问题,源码如下:
2.解决方案
2.1 先hook Toast 进行代理捕捉异常
通过查看源码可知,TN#Handler
是一个hook点,可以对其进行hook 替代,捕捉异常,核心代码如下:
public class SafetToast {
private static final String TAG="SafetToast";
/**
* 处理7.x 的toast 异常 ,代理TN#Handler
* <p>
* toast 源码地址:
* https://cs.android.com/android/platform/superproject/+/android-7.1.0_r1:frameworks/base/core/java/android/widget/Toast.java;bpv=1;bpt=1
*
* @param toast
*/
public static Toast fixToastWithAndroid7(Toast toast) {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1) {
try {
Class<?> toastClass = Toast.class;
Field mTNField = toastClass.getDeclaredField("mTN");
mTNField.setAccessible(true);
Object mTN = mTNField.get(toast);
Field handleField = mTN.getClass().getDeclaredField("mHandler");
handleField.setAccessible(true);
final Handler handler = (Handler) handleField.get(mTN);
Handler proxyHandler = new Handler(handler.getLooper()) {
@Override
public void dispatchMessage(Message msg) {
try {
Log.w(TAG," proxy toast handle");
handler.dispatchMessage(msg);
} catch (Exception e) {
e.printStackTrace();
}
}
};
handleField.set(mTN, proxyHandler);
Log.w(TAG,"fixToastWithAndroid7");
} catch (Exception e) {
e.printStackTrace();
}
}else{
Log.w(TAG," current device not need fix toast");
}
return toast;
}
}
2.2 Lancet aop字节编码进行全局替换:
在实际上开发中,存在各种第三方的sdk, 存在Toast 处理点不同的问题,需要通过Aop方式进行替换。
编写Lancet 核心代码:
public class LancetTools {
private static final String TAG="LancetTools";
@Proxy(value = "show")
@TargetClass(value = "android.widget.Toast")
public void show() {
Toast toast= (Toast) This.get();
SafetToast.fixToastWithAndroid7(toast);
Origin.callVoid();
}
}
以上代码比较简单,在编译过程中,生成dex文件之前,对class文件中每个Toast.show()
进行编码操作,替换成以上代码。进行hook,接着继续调用原有逻辑;
在实际上开发中,存在多个渠道包问题和Lancet 代码修改后会全量编译问题。
最佳的做法是:根据渠道动态加载Lancet 插件和抽象出一个Library模块管理有关Lancet api 代码
若是不存在多渠道包,或者变种包,则进行正常的配置便可。
因项目中在vivo渠道包中使用该功能,进行验证修复效果。
进行以下操作:
在Root目录下的build.gradle中:
buildscript {
//定义一个开关变量, 判断渠道包任务
ext.lancet_open = gradle.startParameter.taskNames.any {
it.contains('vivo')|| it.contains('Vivo')
}
dependencies {
//classpath 'me.ele:lancet-plugin:1.0.6'
// 用于解决asm6问题
classpath 'com.bytedance.tools.lancet:lancet-plugin-asm6:1.0.2'
}
}
在App module中:
apply plugin: 'com.android.application'
if (lancet_open) {
//动态依赖该plugin插件
apply plugin: 'me.ele.lancet'
}
dependencies {
//vivo 渠道中依赖
vivoImplementation project(':lancetLib')
// lancetLib中已经依赖该库,因此不需要再次依赖
//compileOnly 'me.ele:lancet-base:1.0.6'
}
最后创建一个LancetLib的moudle, 编写lancet api 相关的代码:
3.测试验证:
3.1 查看apk中字节编码后代码
项目中原本的代码:
经过lancet aop 字节编码后的代码,查看apk中代码:
3.2 运行Logcat 日志:
在Android 7.1及其以下设备运行:
成功打印日志,进入到hook toast的dispatchMessage()
中,一次Toast 会有一次show 一次hide,因此会打印两遍proxy toast handle
4.进一步学习Lancet 字节编码
Lancet 常用的两种纺织方式:
1.@Insert 指令
:
顾名思义,是在原本函数执行前或者执行后插入一段逻辑,在中转函数中接着调用原本的旧逻辑函数。通常用于项目或者sdk中创建的类。
2.@Proxy指令
:
顾名思义,是代理原本的方法逻辑,进行替换,执行新的逻辑操作(在中转函数中可摒弃旧的函数,也可以继续调用旧的函数)。通用对Android系统类 Api 调用。
匹配目标类:
1.@TargetClass
通过类名来匹配
Scope.SELF
代表仅匹配 value 指定的目标类.
Scope.DIRECT
代表匹配 value 指定类的直接子类.
Scope.All
代表匹配 value 指定类的所有子类.
Scope.LEAF
代表匹配 value 指定类的最终子类.众所周知java是单继承,所以继承关系是树形结构,所以这里代表了指定类为顶点的继承树的所有叶子节点
2.@ImplementedInterface
通过接口来匹配
Scope.SELF
: 代表直接实现所有指定接口的类.
Scope.DIRECT
: 代表直接实现所有指定接口,以及指定接口的子接口的类.
Scope.ALL
: 代表 Scope.DIRECT 指定的所有类及他们的所有子类.
Scope.LEAF
: 代表 Scope.ALL 指定的森林结构中的所有叶节点.
申明方法注意点:
保持 Hook 方法的 public/protected/private static 信息与目标方法一致,参数类型,返回类型与目标方法一致。返回类型可以用 Object 代替。方法名不限.。异常声明也不限。
通过一个案例进一步了解Lancet ,在AppCompatActivity的子类中onStop()执行前插入一段
System.out.println("hello world");
@TargetClass(value = "androidx.appcompat.app.AppCompatActivity", scope = Scope.LEAF)
@Insert(value = "onStop",mayCreateSuper = true)
protected void onStop(){ // 修复符 和static 信息与目标方法一致,参数类型,返回类型与目标方法一致
System.out.println("hello world");
Origin.callVoid();
}
Scope.LEAF
:该类中所有的子类节点上
mayCreateSuper true
: 当该方法没有重写时,会自动重写。
接着构建apk ,查看字节编码后的效果:
更多详细,请阅读Lancet 开源地址