持续创作,加快成长!这是我参加「日新方案 10 月更文挑战」的第6天,点击查看活动概况

前言

很高兴遇见你~

关于 Gradle 学习,我所理解的流程如下图:

Gradle 系列 (六)、Gradle Transform + ASM + Javassist 实战

在本系列的上一篇文章中,咱们介绍了:

1、什么是 Gradle Transform?

2、自界说 Gradle Transform 流程

3、Gradle Transform 数据流程以及中心 Api 分析

4、Gradle Gransform 的增量与并发并封装了一套自界说模版,简化咱们自界说 Gradle Transform 的运用

还没有看过上一篇文章的朋友,主张先去阅览Gradle 系列 (五)、自界说 Gradle Transform,接下来咱们介绍 Gradle Transform + ASM + Javassist 的实战运用

回忆

上一篇文章咱们在前言中留了几个问题:

1、为了对 app 功能做一个全面的评估,咱们需求做 UI,内存,网络等方面的功能监控,怎么做?

2、发现某个第三方库有 bug ,用起来不爽,但又不想拿它的源码修正在从头编译,有什么好的办法?

3、我想在不修正源码的情况下,核算某个办法的耗时,对某个办法做埋点,怎么做?

1 是需求经过 Gradle Transform 去做一个 APM 结构,这个写起来篇幅会过长,后续专门开文章去讲。

咱们首要解决 2,3 这两个问题,在此之前先简略学习点 ASM 和 Javassist 的常识

一、ASM 筑基

1.1、ASM 介绍

ASM 是一个 Java 字节码操作结构。它能被用来动态生成字节码或者对现有的类进行增强。ASM 能够直接生成二进制 class 文件,也能够在类被加载入 Java 虚拟机之前动态改变类行为。比方办法履行前后刺进代码,增加成员变量,修正父类,增加接口等等

1.2、ASM Api

首要先引入 ASM 相关的 Gradle 长途依靠:

//asm
implementation 'org.ow2.asm:asm:9.2'
implementation 'org.ow2.asm:asm-util:9.2'
implementation 'org.ow2.asm:asm-commons:9.2'

ASM 的 Api 有两种运用办法:

1、Tree Api :树形形式

2、Visitor Api :拜访者形式

Tree Api 会将 class 文件的结构读取到内存,构建一个树形结构,在处理 Method,Field 等元素时,会到树形结构中定位到某个元素进行操作,然后把操作在写入 class 文件,最终到达修正字节码的目的。一般比较合适处理杂乱的场景

1.2.1、Visitor Api:拜访者形式

Visitor Api 则是经过接口的办法,分离读 class写 class 的逻辑,一般经过 ClassReader 读取 class ,然后 ClassReader 经过 ClassVisitor 抽象类(ClassWriter 是它的具体完成类),将 class 的每个细节按次序传递给 ClassVisitor(ClassVisitor 中有许多 visitXXX 办法),这个进程就像 ClassReader 带着 ClassVisitor 游览了 class 的每一个指令,有了这些信息,就能够操作这个 class 了

这种办法比较合适处理一些简略的场景,如:出于某个目的,寻觅 class 文件中的一个 hook 点进行字节码修正。咱们就能够运用这种办法

1.2.1.1、ClassVisitor

ClassVisitor 是一个抽象类,首要用于接纳 ClassReader 传递过来的每一个字节码指令,常用的完成类有:ClassWriter。

1.2.1.1.1、ClassVisitor 结构办法

ClassVisitor 结构办法首要有两个:

 public ClassVisitor(final int api) {
    this(api, null);
  }
 public ClassVisitor(final int api, final ClassVisitor classVisitor){
    //...
 }

咱们能够运用:

1、传入 ASM 的 Api 版别去构建它:Opcodes.ASM4, Opcodes.ASM5, Opcodes.ASM6 or Opcodes.ASM7

2、传入 ASM 的 Api 版别和 ClassVisitor 的完成类如:ClassWriter 去构建它

1.2.1.1.2、ClassVisitor visitXXX 系列办法

它还有一系列 visitXXX 办法,罗列常用的几个:

public abstract class ClassVisitor {
  //...
  //一开端调用
  public void visit(
      final int version,
      final int access,
      final String name,//类名,例如:com.dream.androidutil.StringUtils => com/dream/androidutil/StringUtils
      final String signature,//泛型
      final String superName,//父类名
      final String[] interfaces) {//完成的接口
  }
  //拜访注解
  public AnnotationVisitor visitAnnotation(
    //注解称号,例如:com.dream.customannotation.CostTime => Lcom/dream/customannotation/CostTime;
    final String descriptor,
    final boolean visible) {
    //...
  }
  //拜访办法
  public MethodVisitor visitMethod(
      final int access,
      final String name,//办法名,例如:getCharArray
      final String descriptor,//办法签名,简略来说便是办法参数和回来值的特定字符final String signature,//泛型
      final String[] exceptions) {
    if (cv != null) {
      return cv.visitMethod(access, name, descriptor, signature, exceptions);
    }
    return null;
  }
  //拜访结束时调用
  public void visitEnd() {
    if (cv != null) {
      cv.visitEnd();
    }
  }
}
1.2.1.1.3、ClassVisitor visitXXX 办法调用次序

上述办法的调用遵循必定的次序,下面罗列的是一切 visitXXX 办法的调用次序:

visit
[visitSource][visitModule][visitNestHost][visitPermittedSubclass][visitOuterClass]
(
 visitAnnotation |
 visitTypeAnnotation |
 visitAttribute
)*
(
 visitNestMember |
 visitInnerClass |
 visitRecordComponent |
 visitField |
 visitMethod
)* 
visitEnd

其中,涉及到一些符号,它们的含义如下:

[] :表明最多调用一次,能够不调用,但最多调用一次。

() 和 | :表明在多个办法之间,能够挑选恣意一个,而且多个办法之间不分前后次序

* : 表明办法能够调用0次或多次。

简化一下,如下示例:

visit
(
 visitAnnotation |
)* 
(
 visitField |
 visitMethod
)* 
visitEnd

解说阐明:上述代码会先调用visit办法,接着调用visitAnnotation 办法,然后在调用visitFieldvisitMethod办法,最终调用visitEnd办法

1.2.1.2、ClassReader

ClassReader 首要用于读取 class 文件,并把每个字节码指令传递给 ClassVisitor 的 visitXXX 办法

1.2.1.2.1、ClassReader 结构办法

如下图:

Gradle 系列 (六)、Gradle Transform + ASM + Javassist 实战

咱们能够运用:

1、ByteArray(字节数组)

2、inputStream(输入流),

3、className(String 的类称号)

等来构建它

1.2.1.2.2、ClassReader 办法

ClassReader 供给了一系列 get 办法获取类信息:

Gradle 系列 (六)、Gradle Transform + ASM + Javassist 实战

不过,它最重要的办法还是 accept 办法:

Gradle 系列 (六)、Gradle Transform + ASM + Javassist 实战

accept 能够接纳一个 ClassVisitor 和一个 parsingOptions。parsingOptions 取值如下:

0:会生成一切的ASM代码,包括调试信息、frame信息和代码信息。

ClassReader.SKIP_CODE:会疏忽代码信息,例如:会疏忽关于 MethodVisitor.visitXxxInsn() 办法的调用

ClassReader.SKIP_DEBUG:会疏忽调试信息,例如:会疏忽关于MethodVisitor.visitParameter()、MethodVisitor.visitLineNumber()等办法的调用。

ClassReader.SKIP_FRAMES:会疏忽 frame 信息,例如:会疏忽关于MethodVisitor.visitFrame()办法的调用。

ClassReader.EXPAND_FRAMES:会对frame信息进行扩展,例如:会对 MethodVisitor.visitFrame() 办法的参数有影响。

Tips: 引荐运用ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES,由于运用这样的一个值,能够生成最少的 ASM 代码,可是又能完成完整的功能

接纳后便开端读取数据。当满足必定条件时,就会触发 ClassVisitor 下的 visitXXX 办法。如下示例:

package com.dream.gradletransformdemo;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import java.io.IOException;
public class Test {
    public static void main(String[] var0) throws IOException {
        ClassReader cr = new ClassReader("java.util.ArrayList");
        cr.accept(new MyClassVisitor(Opcodes.ASM7),ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
    }
    static class MyClassVisitor extends ClassVisitor {
        public MyClassVisitor(int api) {
            super(api);
        }
        @Override
        public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
            System.out.println("visitMethod: " + "access=>" + access + " name=>" + name
                    + " descriptor=>" + descriptor);
            return super.visitMethod(access, name, descriptor, signature, exceptions);
        }
    }
}

咱们承继了ClassVisitor 类并重写了visitMethod 办法。还记得之前所说的吗?ClassVistor 界说了在读取 class 时会触发的 visitXXX 办法。经过 accept 办法,建立了 ClassVisitor 与 ClassReader 之间的衔接。因而,当 ClassReader 拜访目标的办法时,它将触发ClassVisitor 内的 visitMethod 办法,这时由于咱们在 visitMethod 下增加了一条打印句子,因而会打印如下信息:

//打印成果,由于 ArrayList 办法众多,简略截取看几个常用的
//...
//<init> 对应的便是 ArrayList 的结构办法,咱们能够看到有三个
visitMethod: access=>1 name=><init> descriptor=>(I)V
visitMethod: access=>1 name=><init> descriptor=>()V
visitMethod: access=>1 name=><init> descriptor=>(Ljava/util/Collection;)V
//ArrayList get 办法
visitMethod: access=>1 name=>get descriptor=>(I)Ljava/lang/Object;
//ArrayList set 办法
visitMethod: access=>1 name=>set descriptor=>(ILjava/lang/Object;)Ljava/lang/Object;
//ArrayList add 办法,有 3 个
visitMethod: access=>2 name=>add descriptor=>(Ljava/lang/Object;[Ljava/lang/Object;I)V
visitMethod: access=>1 name=>add descriptor=>(Ljava/lang/Object;)Z
visitMethod: access=>1 name=>add descriptor=>(ILjava/lang/Object;)V
//ArrayList remove 办法                                                                                                      
visitMethod: access=>1 name=>remove descriptor=>(I)Ljava/lang/Object;
visitMethod: access=>1 name=>remove descriptor=>(Ljava/lang/Object;)Z                                                                                                      
//...                                                                                                       
1.2.1.2.3、字段解析

上述打印成果:

<init> :表明一个类结构函数的名字

access :办法的拜访控制符的界说

name :办法名

descriptor :办法签名,简略来说便是办法参数和回来值的特定字符串

咱们挑两个 descriptor 进行解析:

//1、() 里边的表明参数
//2、I 表明 int,
//3、假如不是根底类型,需求写完整包名,同时以 L 打头,例如 Object 对应:Ljava/lang/Object;
//4、V 表明 void
//因而咱们能够知道这个 descriptor :接纳两个参数:int,Object ,回来值为:void
descriptor=>(ILjava/lang/Object;)V
//1、() 里边的表明参数,() 外面的表明回来值
//2、假如是特定的类,需求写完整包名,同时以 L 打头,例如 Object 对应:Ljava/lang/Object;
//4、Z 表明 boolean
//因而咱们能够知道这个 descriptor :接纳一个参数:Object ,回来值为:boolean
descriptor=>(Ljava/lang/Object;)Z

留意 () 里边的参数

1、没有的话就什么都不写

2、有的话,假如不是根底类型,要是以 L 打头的类名(包括包名)

3、关于数组以 [ 打头,

4、假如不是根底类型,多个参数之间用分号;进行分隔,即便只要一个参数,也要写分号

类型对应表:

Type Descriptor Java Type
Z boolean
C char
B byte
S short
I int
F float
J long
D double
Ljava/lang/Object; Object
[I int[]
[[Ljava/lang/Object Object[][]

1.2.1.3、ClassWriter

ClassWriter 的父类是 ClassVisitor ,因而承继了 ClassVisitor 的 visitXXX 系列办法,首要用于字节码的写入

1.2.1.3.1、ClassWriter 结构办法

ClassWriter 的结构办法有两个:

public ClassWriter(final int flags) {
  this(null, flags);
}
public ClassWriter(final ClassReader classReader, final int flags) {
  //...
}

咱们能够运用:

1、flags

2、classReader + flags

来构建它,其中 flags 的取值如下:

0 :ASM 不会主动核算 max stacks 和 max locals,也不会主动核算 stack map frames

ClassWriter.COMPUTE_MAXS :ASM 会主动核算 max stacks 和 max locals,但不会主动核算 stack map frames

ClassWriter.COMPUTE_FRAMES :ASM 会主动核算 max stacks 和 max locals,也会主动核算 stack map frames

Tips: 主张运用 ClassWriter.COMPUTE_FRAMES,核算速度快,履行效率高

1.2.1.3.2、toByteArray 办法

这个办法的作用是将咱们之前对 class 的修正(visitXXX 内部修正字节码)转换成 byte 数组,然后经过输出流写入到文件,这样就到达了修正字节码的目的

ok,ASM 的常识点就介绍这么多,接下来咱们看下 Javassist

二、Javassist 筑基

简略介绍下 Javassist,由于它和 Java 的反射 Api 很像,上手简略一些,咱们直接代码中去感受一下,写了详细的注释

首要先增加 Javassist Gradle 长途依靠:

implementation 'org.javassist:javassist:3.29.2-GA'

2.1、运用 Javassist 生成 class 文件

1、首要供给一个待生成的 class 文件模版,如下:

package com.dream.gradletransformdemo;
public class Person {
    //私有特点 name,初始值:erdai
    private String name = "erdai";
    //name get 办法
    public void setName(String var1) {
        this.name = var1;
    }
    //name set 办法
    public String getName() {
        return this.name;
    }
    //无参结构办法,办法体:this.name = "xiaoming";
    public Person() {
        this.name = "xiaoming";
    }
    //一个参数的结构办法,办法提:this.name = var1;
    public Person(String var1) {
        this.name = var1;
    }
    //普通办法:printName,办法提:System.out.println(this.name);
    public void printName() {
        System.out.println(this.name);
    }
}

2、编写 Javassist 代码生成 Person.class 文件

Javassist 生成 .class 文件和 JavaPoet 生成 .java 文件十分相似,假如你了解 JavaPoet 的话,下面生成进程将会变得十分简略

public class TestCreateClass {
    /**
     * 创立一个 Person.class 文件
     */
    public static CtClass createPersonClass() throws Exception {
        ClassPool pool = ClassPool.getDefault();
        // 1. 创立一个空类:Person ,包名:com.dream.gradletransformdemo
        CtClass cc = pool.makeClass("com.dream.gradletransformdemo.Person");
        // 2. 新增一个字段 private String name;
        // 字段名为 name
        CtField param = new CtField(pool.get("java.lang.String"), "name", cc);
        // 拜访级别是 private
        param.setModifiers(Modifier.PRIVATE);
        // 初始值是 "erdai"
        cc.addField(param, CtField.Initializer.constant("erdai"));
        // 3. 生成 setter、getter 办法
        cc.addMethod(CtNewMethod.setter("setName", param));
      	cc.addMethod(CtNewMethod.getter("getName", param));
        // 4. 增加无参的结构函数
        CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);
      	//办法体:this.name = "xiaoming";
        cons.setBody("{name = \"xiaoming\";}");
        cc.addConstructor(cons);
        // 5. 增加有参的结构函数
        cons = new CtConstructor(new CtClass[]{pool.get("java.lang.String")}, cc);
        // 办法提:this.name = var1; 
      	//$0=this \$1,$2,$3... 代表办法参数
        cons.setBody("{$0.name = $1;}");
        cc.addConstructor(cons);
        // 6. 创立一个名为 printName 的办法,无参数,无回来值,输出 name 值
        CtMethod ctMethod = new CtMethod(CtClass.voidType, "printName", new CtClass[]{}, cc);
        ctMethod.setModifiers(Modifier.PUBLIC);
        ctMethod.setBody("{System.out.println(this.name);}");
        cc.addMethod(ctMethod);
        // 将 Person.class 文件输出到如下文件夹
        cc.writeFile("/Users/zhouying/AndroidStudioProjects/MixDemo/GradleTransformDemo/app/src/main/java/");
      	return cc;
    }
    public static void main(String[] args) {
        try {
            createPersonClass();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

2.2、运用 Javassist 调用生成的类目标

首要有三种办法:

1、经过生成类时创立的 CtClass 实例目标获取 Class 目标,然后经过反射调用

2、经过读取生成类的方位生成 CtClass 实例目标,在经过 CtClass 实例目标获取 Class 目标,然后经过反射调用

3、经过界说一个新接口的办法

以上面生成的类为例,咱们来调用一下它

1、经过生成类时创立的 CtClass 实例目标获取 Class 目标,然后经过反射调用

public class TestCreateClass {
    //...
    //修正 main 办法
    public static void main(String[] args) {
        try {
            CtClass ctClass = createPersonClass();
            //将 ctClass 转换成 Class 目标,这样咱们就能够愉快的运用反射拉
            Class<?> clazz = ctClass.toClass();
            Object o = clazz.newInstance();
            //调用 Person 的 set 办法将 name 设为:erdai666
            Method setNameMethod = clazz.getDeclaredMethod("setName",String.class);
            setNameMethod.invoke(o,"erdai666");
            //调用 printName 办法打印出来
            Method printNameMethod = clazz.getDeclaredMethod("printName");
            printNameMethod.invoke(o);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
//运转后,打印成果
erdai666

2、经过读取生成类的方位生成 CtClass 实例目标,在经过 CtClass 实例目标获取 Class 目标,然后经过反射调用

public class TestCreateClass {
    //...
    //修正 main 办法
    public static void main(String[] args) {
        try {
            ClassPool classPool = ClassPool.getDefault();
            //经过生成类的必定方位构建 CtClass 实例目标
            classPool.appendClassPath
                    ("/Users/zhouying/AndroidStudioProjects/MixDemo/GradleTransformDemo/app/src/main/java/");
            CtClass ctClass = classPool.get("com.dream.gradletransformdemo.Person");
            //将 ctClass 转换成 Class 目标,这样咱们就能够愉快的运用反射拉
            Class<?> clazz = ctClass.toClass();
            Object o = clazz.newInstance();
            //调用 Person 的 set 办法将 name 设为:erdai666
            Method setNameMethod = clazz.getDeclaredMethod("setName",String.class);
            setNameMethod.invoke(o,"erdai666");
            //调用 printName 办法打印出来
            Method printNameMethod = clazz.getDeclaredMethod("printName");
            printNameMethod.invoke(o);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
//运转后,打印成果
erdai666

3、经过界说一个新接口的办法

interface IPerson{
    void setName(String name);
    String getName();
    void printName();
}
public class TestCreateClass {
    //...
    //修正 main 办法
    public static void main(String[] args) {
        try {
            ClassPool classPool = ClassPool.getDefault();
            //经过生成类的必定方位构建 CtClass 实例目标
           classPool.appendClassPath
                    ("/Users/zhouying/AndroidStudioProjects/MixDemo/GradleTransformDemo/app/src/main/java/");
            //获取接口
            CtClass iPersonCtClass = classPool.get("com.dream.gradletransformdemo.IPerson");
            //获取生成的类 Person
            CtClass personCtClass = classPool.get("com.dream.gradletransformdemo.Person");
            //让 Person 完成 IPerson 接口
            personCtClass.setInterfaces(new CtClass[]{iPersonCtClass});
            //接下俩就能够经过接口进行调用了
            IPerson person = (IPerson) personCtClass.toClass().newInstance();
            person.setName("erdai666");
            person.printName();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
//运转后,打印成果
erdai666

2.3、Javassist 修正现有的类目标

一般咱们会运用这种办法结合 Gradle Transform 完成对现有类的插桩

1、首要咱们先创立一个 PersonService.java 的文件,内容如下:

package com.dream.gradletransformdemo;
public class PersonService {
    public void getPerson(){
        System.out.println("get Person");
    }
    public void personFly(){
        System.out.println("I believe i can fly...");
    }
}

2、接下来运用 Javassist 对它进行修正

public class TestUpdatePersonService {
    public static void main(String[] args) {
        try {
            ClassPool pool = ClassPool.getDefault();
            CtClass ctClass = pool.get("com.dream.gradletransformdemo.PersonService");
            CtMethod personFly = ctClass.getDeclaredMethod("personFly");
            //在 personFly 办法的前后刺进代码
            //多行句子写法:
            //"{System.out.println(\"起飞之前预备降落伞\");System.out.println(\"起飞之前预备降落伞111\");}"
            personFly.insertBefore("System.out.println(\"起飞之前预备降落伞\");");
            personFly.insertAfter("System.out.println(\"成功落地...\");");
            //新增一个办法
            CtMethod ctMethod = new CtMethod(CtClass.voidType,"joinFriend",new CtClass[]{},ctClass);
            ctMethod.setModifiers(Modifier.PUBLIC);
            ctMethod.setBody("System.out.println(\"I want to be your friend\");");
            ctClass.addMethod(ctMethod);
            //获取类目标,接下来就能够愉快的运用反射了
            Class<?> clazz = ctClass.toClass();
            Object o = clazz.newInstance();
            //调用 personFly 办法
            Method personFlyMethod = clazz.getDeclaredMethod("personFly");
            personFlyMethod.invoke(o);
            //调用 joinFriend 办法
            Method joinFriendMethod = clazz.getDeclaredMethod("joinFriend");
            joinFriendMethod.invoke(o);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
//运转后,打印成果
起飞之前预备降落伞
I believe i can fly...
成功落地...
I want to be your friend

需求留意的是: 上面的insertBeforeinsertAftersetBody中的句子,假如你是单行句子能够直接用双引号,可是有多行句子的情况下,你需求将多行句子用{}括起来。Javassist 只承受单个句子或用大括号括起来的句子块

接下来咱们进入实战环节

三、Gradle Transform + Javassist 实战

首要看下咱们要解决的第一个问题:发现某个第三方库有 bug ,用起来不爽,但又不想拿它的源码修正在从头编译,有什么好的办法?

我的思路:运用 Gradle Transform + Javassit 修正库里边办法的内部完成,等于没说,,咱们来实操一下

首要引入一个我预备好的第三方库:

在项目的根 build.gradle 参加 Jitpack 库房:

allprojects {
    repositories {
     	//...
        maven { url 'https://jitpack.io' }
    }
}

在 app 的 build.gradle 中增加如下依靠:

implementation 'com.github.sweetying520:AndroidUtils:1.0.7'

ok,接着咱们看下这个库中 StringUtils 的源码:

Gradle 系列 (六)、Gradle Transform + ASM + Javassist 实战

嗯,就两个简略的工具类,咱们在 MainActivity 中运用一下:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        StringUtils.getCharArray(null)
        StringUtils.getLength(null)
    }
}

运转项目,你会发现 Crash 了,查看 Log 日志发现是空指针反常

查看第三方库发现这两个办法没有做空判断,传 null,程序必定就 Crash 了,咱们必定不能允许这种工作产生,当然你能够直接修正源码后从头发布,可是这种办法太简略了,学习咱们就应该不断的去挑战自己,想一些立异的思路,今天咱们就站在修正字节码的视点去修正它

自界说一个 Transform 承继咱们上篇文章写的 Transform 模版,运用 Javassist 进行插桩,代码如下:

class FixThirdLibTransform : BaseCustomTransform(true) {
    /**
     * 获取 Transform 称号
     */
    override fun getName(): String {
        return "FixThirdLibTransform"
    }
    /**
     * 只处理 StringUtils.class 文件,其他的都给过滤掉
     */
    override fun classFilter(className: String) = className.endsWith("StringUtils.class")
    /**
     * 用于过滤 Variant,回来 false 表明 Variant 不履行该 Transform
     */
    @Incubating
    override fun applyToVariant(variant: VariantInfo?): Boolean {
        return "debug" == variant?.buildTypeName
    }
    /**
     * 经过此办法进行字节码插桩
     */
    override fun provideFunction() = { input: InputStream, output: OutputStream ->
        try {
            val classPool = ClassPool.getDefault()
            val makeClass = classPool.makeClass(input)
            //对 StringUtils 的 getLength 进行插桩
            val getLengthMethod = makeClass.getDeclaredMethod("getLength")
            getLengthMethod.insertBefore("{System.out.println(\"Hello getLength bug修正了..\");if($1==null)return 0;}")
            //对 StringUtils 的 getCharArray 进行插桩
            val getCharArrayMethod = makeClass.getDeclaredMethod("getCharArray")
            getCharArrayMethod.insertBefore("{System.out.println(\"Hello getCharArray bug修正了..\");if($1==null)return new char[0];}")
            //打印 log,此 log 是 BaseCustomTransform 里边的
            log("插桩的类名:${makeClass.name}")
            makeClass.declaredMethods.forEach {
                log("插桩的办法名:$it")
            }
            output.write(makeClass.toBytecode())
            makeClass.detach()
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }
}

在 CustomTransformPlugin 进行插件的注册:

class CustomTransformPlugin: Plugin<Project> {
    override fun apply(project: Project) {
      	//...
        // 1、获取 Android 扩展
        val androidExtension = project.extensions.getByType(AppExtension::class.java)
        // 2、注册 Transform
      	//...
        androidExtension.registerTransform(FixThirdLibTransform())
    }
}

发布一个新的插件版别,修正根 build.gradle 插件的版别,同步后从头运转 app,作用验证:

1、先看一眼咱们自界说 Transform 里边的 log 打印,契合预期:

Gradle 系列 (六)、Gradle Transform + ASM + Javassist 实战

2、在看下 app 作用,没有奔溃,契合预期

Gradle 系列 (六)、Gradle Transform + ASM + Javassist 实战

3、最终看一眼咱们插桩的 log 日志,契合预期

Gradle 系列 (六)、Gradle Transform + ASM + Javassist 实战

四、Gradle Transform + ASM 实战

接下来咱们运用 Gradle Transform + ASM 解决第二个问题:我想在不修正源码的情况下,核算某个办法的耗时,对某个办法做埋点,怎么做?

就以 MainActivity 的 onCreate 办法为比如,咱们核算一下 onCreate 办法的耗时

1、首要需求咱们先装置一个插件:ASM Bytecode Viewer Support Kotlin ,这个插件能帮助咱们快速的进行 ASM 字节码插桩的操作

翻开 MainActivity ,看一眼 onCreate 办法插桩之前的代码:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        StringUtils.getCharArray(null)
        StringUtils.getLength(null)
    }
}

右键挑选:ASM Bytecode Viewer

Gradle 系列 (六)、Gradle Transform + ASM + Javassist 实战

会生成如下代码,挑选 ASMified ,就能够看到 ASM 字节码了

Gradle 系列 (六)、Gradle Transform + ASM + Javassist 实战

留意:咱们要操作的是 ASM 字节码,而非 Java 字节码,其实二者十分挨近,只不过 ASM 字节码是用 Java 代码的形式来描绘的

2、修正 MainActivity onCreate 办法代码

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        val startTime = SystemClock.elapsedRealtime()
        Log.d("erdai", "onCreate startTime: $startTime")
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        StringUtils.getCharArray(null)
        StringUtils.getLength(null)
        val endTime = SystemClock.elapsedRealtime()
        Log.d("erdai", "onCreate endTime: $endTime")
        val cost = endTime - startTime
        Log.d("erdai", "onCreate 耗时: $cost")
    }
}

从头查看 ASM 字节码,然后点击:Show differences ,就会出来前后两次代码的比照,绿色部分代码便是咱们要增加的

Gradle 系列 (六)、Gradle Transform + ASM + Javassist 实战

3、创立一个 Transform 承继 BaseTransform ,编写 ASM 代码:

class CostTimeTransform: BaseCustomTransform(true) {
    /**
     * 获取 Transform 称号
     */
    override fun getName(): String {
        return "CostTimeTransform"
    }
    /**
     * 过滤只核算以 Activity.class 结尾的文件
     */
    override fun classFilter(className: String) = className.endsWith("Activity.class")
    /**
     * 用于过滤 Variant,回来 false 表明 Variant 不履行该 Transform
     */
    @Incubating
    override fun applyToVariant(variant: VariantInfo?): Boolean {
        return "debug" == variant?.buildTypeName
    }
    /**
     * 经过此办法进行字节码插桩
     */
    override fun provideFunction() = { input: InputStream,output: OutputStream ->
        //运用 input 输入流构建 ClassReader
        val reader = ClassReader(input)
        //运用 ClassReader 和 flags 构建 ClassWriter
        val writer = ClassWriter(reader, ClassWriter.COMPUTE_FRAMES)
        //运用 ClassWriter 构建咱们自界说的 ClassVisitor
        val visitor = CostTimeClassVisitor(writer)
        //最终经过 ClassReader 的 accept 将每一条字节码指令传递给 ClassVisitor
        reader.accept(visitor, ClassReader.SKIP_DEBUG or  ClassReader.SKIP_FRAMES)
        //将修正后的字节码文件转换成字节数组
        val byteArray = writer.toByteArray()
        //最终经过输出流修正文件,这样就完成了字节码的插桩
        output.write(byteArray)
    }
}

4、中心逻辑的处理是在咱们自界说的 ClassVisitor 中:

package com.dream.customtransformplugin
import org.objectweb.asm.*
import org.objectweb.asm.commons.AdviceAdapter
class CostTimeClassVisitor(nextVisitor: ClassVisitor) : ClassVisitor(Opcodes.ASM6,nextVisitor) {
    override fun visitMethod(
        access: Int,
        name: String?,
        descriptor: String?,
        signature: String?,
        exceptions: Array<out String>?
    ): MethodVisitor {
        val visitor = super.visitMethod(access, name, descriptor, signature, exceptions)
        // AdviceAdapter 是 MethodVisitor 的子类,运用 AdviceAdapter 能够更便利的修正办法的字节码。
        // AdviceAdapter其中几个重要办法如下:
        // void visitCode():表明 ASM 开端扫描这个办法
        // void onMethodEnter():进入这个办法
        // void onMethodExit():行将从这个办法出去
        // void onVisitEnd():表明办法扫描完毕
        return object : AdviceAdapter(Opcodes.ASM6, visitor, access, name, descriptor) {
            //在原办法代码的前面刺进代码
            override fun onMethodEnter() {
                visitMethodInsn(INVOKESTATIC, "android/os/SystemClock", "elapsedRealtime", "()J", false)
                visitVarInsn(LSTORE, 2)
                visitLdcInsn("erdai")
                visitTypeInsn(NEW, "java/lang/StringBuilder")
                visitInsn(DUP)
                visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false)
                visitLdcInsn("onCreate startTime: ")
                visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false)
                visitVarInsn(LLOAD, 2)
                visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false)
                visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false)
                visitMethodInsn(INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false)
                visitInsn(POP)
            }
            //在原办法代码的后边刺进代码
            override fun onMethodExit(opcode: Int) {
                visitMethodInsn(INVOKESTATIC, "android/os/SystemClock", "elapsedRealtime", "()J", false)
                visitVarInsn(LSTORE, 4)
                visitLdcInsn("erdai")
                visitTypeInsn(NEW, "java/lang/StringBuilder")
                visitInsn(DUP)
                visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false)
                visitLdcInsn("onCreate endTime: ")
                visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false)
                visitVarInsn(LLOAD, 4)
                visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false)
                visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false)
                visitMethodInsn(INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false)
                visitInsn(POP)
                visitVarInsn(LLOAD, 4)
                visitVarInsn(LLOAD, 2)
                visitInsn(LSUB)
                visitVarInsn(LSTORE, 6)
                visitLdcInsn("erdai")
                visitTypeInsn(NEW, "java/lang/StringBuilder")
                visitInsn(DUP)
                visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false)
                visitLdcInsn("onCreate \u8017\u65f6: ")
                visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false)
                visitVarInsn(LLOAD, 6)
                visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false)
                visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false)
                visitMethodInsn(INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false)
                visitInsn(POP)
            }
        }

这样咱们在 Activity 中刺进办法的耗时就已经完成了,可是会面对一个问题:刚说的 Activity 是一切的 Activity 包括系统的办法是一切的办法 ,这种作用必定不是我想要的,而且还可能会出问题,因而咱们这儿能够加一个自界说注解去控制一下,只核算增加了注解办法的耗时

5、在主工程中创立一个自界说注解

package com.dream.gradletransformdemo.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
 * 自界说办法耗时注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CostTime {
}

6、接着对 CostTimeClassVisitor 代码进行修正:

class CostTimeClassVisitor(nextVisitor: ClassVisitor) : ClassVisitor(Opcodes.ASM6,nextVisitor) {
    //是否需求被 hook
    var isHook = false
    override fun visitMethod(
        access: Int,
        name: String?,
        descriptor: String?,
        signature: String?,
        exceptions: Array<out String>?
    ): MethodVisitor {
        val visitor = super.visitMethod(access, name, descriptor, signature, exceptions)
        return object : AdviceAdapter(Opcodes.ASM6, visitor, access, name, descriptor) {
            /**
             * 拜访自界说注解
             */
            override fun visitAnnotation(descriptor: String, visible: Boolean): AnnotationVisitor? {
              	//假如增加了自界说注解,则进行 hook
                if ("Lcom/dream/gradletransformdemo/annotation/CostTime;" == descriptor) {
                    isHook = true
                }
                return super.visitAnnotation(descriptor, visible)
            }
            //在原办法代码的前面刺进代码
            override fun onMethodEnter() {
                if(!isHook)return
              	//...
            }
            //在原办法代码的后边刺进代码
            override fun onMethodExit(opcode: Int) {
                if(!isHook)return
                //...
            }
        }
    }
}

ok,至此,咱们自界说 Gradle Transform 就编写完成了

在 CustomTransformPlugin 进行插件的注册:

class CustomTransformPlugin: Plugin<Project> {
    override fun apply(project: Project) {
      	//...
        // 1、获取 Android 扩展
        val androidExtension = project.extensions.getByType(AppExtension::class.java)
        // 2、注册 Transform
      	//...
        androidExtension.registerTransform(CostTimeTransform())
    }
}

发布一个新的插件版别,修正根 build.gradle 插件的版别,同步后从头运转 app,作用验证:

1、看控制台 log 日志打印,契合预期:

Gradle 系列 (六)、Gradle Transform + ASM + Javassist 实战

2、接着经过反编译工具看下咱们插桩后的 MainActivity ,契合预期:

Gradle 系列 (六)、Gradle Transform + ASM + Javassist 实战

五、AGP 8.0 版别关于 Gradle Transform 的替换方案

Google 在 AGP 8.0 会将 Gradle Transform 给移除,因而假如项目升级了 AGP 8.0,就需求做好 Gradle Transform 的兼容。

Gradle Transform被抛弃之后,它的代替品是Transform ActionTransform API是由AGP供给的,而Transform Action则是由Gradle 供给。不光是 AGP 需求 TransformJava 也需求,所以由 Gradle 来供给统一的 Transform API 也入情入理,关于 Transform Action不计划介绍,有兴趣的能够去看这篇文章Transform 被抛弃,TransformAction 了解一下~

咱们首要介绍一下 AGP 给咱们供给的 AsmClassVisitorFactory

5.1、AsmClassVisitorFactory 介绍

1、AsmClassVisitorFactory 就好比咱们之前写的自界说 Transform 模版,只不过现在是由官方供给了,里边做了大量的封装:输入文件遍历、加解压、增量,并发等,简化咱们的一个运用。根据官方的说法,AsmClassVisitoFactory 会带来约18%的功能提升,同时能够削减约 5 倍代码。

2、另外从命名也能够看出,Google 更加引荐咱们运用 ASM 进行字节码的插桩

5.2、AsmClassVisitorFactory 运用

接下来咱们就替换一下 Gradle Transform + ASM 的完成方案,运用 AsmClassVisitorFactory 真的是十分简略:

1、自界说一个抽象类承继 AsmClassVisitorFactory,然后 createClassVisitor 办法回来之前写的 CostTimeClassVisitor 即可

package com.dream.customtransformplugin.foragp8
import com.android.build.api.instrumentation.*
import com.dream.customtransformplugin.costtime.CostTimeClassVisitor
import org.objectweb.asm.ClassVisitor
abstract class CostTimeASMFactory: AsmClassVisitorFactory<InstrumentationParameters.None> {
    override fun createClassVisitor(
        classContext: ClassContext,
        nextClassVisitor: ClassVisitor
    ): ClassVisitor {
        return CostTimeClassVisitor(nextClassVisitor)
    }
    override fun isInstrumentable(classData: ClassData): Boolean {
        return true
    }
}

2、接下来在 CustomTransformPlugin 进行注册,注册运用了一种新的办法:

Gradle 系列 (六)、Gradle Transform + ASM + Javassist 实战

3、发布一个新的插件版别,修正根 build.gradle 插件的版别,同步后从头运转 app,作用是一样的

一些不同点:

1、编译使命的 Task 称号变了:

Gradle 系列 (六)、Gradle Transform + ASM + Javassist 实战

2、咱们编译生成的中间产物有了 Asm 相关的文件夹,便利咱们一个作用的验证

Gradle 系列 (六)、Gradle Transform + ASM + Javassist 实战

需求留意的是: Kotlin 文件看不到插桩的代码,淦

六、总结

本篇文章咱们介绍了:

1、ASM Visitor Api 中的几个中心类:

1、ClassVisitor

2、ClassReader

3、ClassWriter

2、Javassist 相关语法,运用起来挨近原生的反射 Api ,比较容易上手

3、自界说 GradleTransform + Javassist 完成了修正第三方库的源码

4、自界说 GradleTransform + ASM 完成了 MainActivity onCreate 办法耗时的核算

5、介绍了 AGP 8.0 Gradle Transform 被移除后运用 AsmClassVisitorFactory 进行适配,进程十分简略

好了,本篇文章到这儿就结束了,希望能给你带来帮助

Github Demo 地址 , 咱们能够结合 demo 一同看,作用杠杠滴

感谢你阅览这篇文章

参考和引荐

Gradle Transform + ASM 探索

Transform 被抛弃,TransformAction 了解一下~

Transform 被抛弃,ASM 怎么适配?

javassist详解

你的点赞,评论,是对我巨大的鼓舞!

欢迎关注我的大众号: sweetying ,文章更新可第一时间收到

假如有问题,大众号内有加我微信的入口,在技术学习、个人成长的道路上,咱们一同行进!