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 开源地址

猜你喜欢

转载自blog.csdn.net/hexingen/article/details/129689734