前言 很高兴遇见你~
关于 Gradle 学习,我所理解的流程如下图:
在本系列的上一篇文章中,我们介绍了:
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 远程依赖:
1 2 3 4 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 构造方法主要有两个:
1 2 3 4 5 6 7 8 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 方法,列举常用的几个:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 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 方法的调用顺序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 visit [visitSource][visitModule][visitNestHost][visitPermittedSubclass][visitOuterClass] ( visitAnnotation | visitTypeAnnotation | visitAttribute )* ( visitNestMember | visitInnerClass | visitRecordComponent | visitField | visitMethod )* visitEnd
其中,涉及到一些符号,它们的含义如下:
[]
:表示最多调用一次,可以不调用,但最多调用一次。
() 和 |
:表示在多个方法之间,可以选择任意一个,并且多个方法之间不分前后顺序
*
: 表示方法可以调用0次或多次。
简化一下,如下示例:
1 2 3 4 5 6 7 8 9 visit ( visitAnnotation | )* ( visitField | visitMethod )* visitEnd
解释说明:上述代码会先调用visit
方法,接着调用visitAnnotation
方法,然后在调用visitField
或visitMethod
方法,最后调用visitEnd
方法
1.2.1.2、ClassReader ClassReader 主要用于读取 class 文件,并把每个字节码指令传递给 ClassVisitor 的 visitXXX 方法
1.2.1.2.1、ClassReader 构造方法 如下图:
我们可以使用:
1、ByteArray(字节数组)
2、inputStream(输入六),
3、className(String 的 类名称)
等来构建它
1.2.1.2.2、ClassReader 方法 ClassReader 提供了一系列 get 方法获取类信息:
不过,它最重要的方法还是 accept 方法:
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 方法。如下示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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 下添加了一条打印语句,因此会打印如下信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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 visitMethod: access=>1 name=>get descriptor=>(I)Ljava/lang/Object; visitMethod: access=>1 name=>set descriptor=>(ILjava/lang/Object;)Ljava/lang/Object; 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 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 3 4 5 6 7 8 9 10 11 12 descriptor=>(ILjava/lang/Object;)V 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 的构造方法有两个:
1 2 3 4 5 6 7 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 远程依赖:
1 implementation 'org.javassist:javassist:3.29.2-GA'
2.1、使用 Javassist 生成 class 文件 1、首先提供一个待生成的 class 文件模版,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package com.dream.gradletransformdemo;public class Person { private String name = "erdai" ; public void setName (String var1) { this .name = var1; } public String getName () { return this .name; } public Person () { this .name = "xiaoming" ; } public Person (String var1) { this .name = var1; } public void printName () { System.out.println(this .name); } }
2、编写 Javassist 代码生成 Person.class 文件
Javassist 生成 .class 文件和 JavaPoet 生成 .java 文件非常类似,如果你熟悉 JavaPoet 的话,下面生成过程将会变得非常简单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 public class TestCreateClass { public static CtClass createPersonClass () throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.makeClass("com.dream.gradletransformdemo.Person" ); CtField param = new CtField (pool.get("java.lang.String" ), "name" , cc); param.setModifiers(Modifier.PRIVATE); cc.addField(param, CtField.Initializer.constant("erdai" )); cc.addMethod(CtNewMethod.setter("setName" , param)); cc.addMethod(CtNewMethod.getter("getName" , param)); CtConstructor cons = new CtConstructor (new CtClass []{}, cc); cons.setBody("{name = \"xiaoming\";}" ); cc.addConstructor(cons); cons = new CtConstructor (new CtClass []{pool.get("java.lang.String" )}, cc); cons.setBody("{$0.name = $1;}" ); cc.addConstructor(cons); CtMethod ctMethod = new CtMethod (CtClass.voidType, "printName" , new CtClass []{}, cc); ctMethod.setModifiers(Modifier.PUBLIC); ctMethod.setBody("{System.out.println(this.name);}" ); cc.addMethod(ctMethod); 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 对象,然后通过反射调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class TestCreateClass { public static void main (String[] args) { try { CtClass ctClass = createPersonClass(); Class<?> clazz = ctClass.toClass(); Object o = clazz.newInstance(); Method setNameMethod = clazz.getDeclaredMethod("setName" ,String.class); setNameMethod.invoke(o,"erdai666" ); Method printNameMethod = clazz.getDeclaredMethod("printName" ); printNameMethod.invoke(o); } catch (Exception e) { e.printStackTrace(); } } } erdai666
2、通过读取生成类的位置生成 CtClass 实例对象,在通过 CtClass 实例对象获取 Class 对象,然后通过反射调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public class TestCreateClass { public static void main (String[] args) { try { ClassPool classPool = ClassPool.getDefault(); classPool.appendClassPath ("/Users/zhouying/AndroidStudioProjects/MixDemo/GradleTransformDemo/app/src/main/java/" ); CtClass ctClass = classPool.get("com.dream.gradletransformdemo.Person" ); Class<?> clazz = ctClass.toClass(); Object o = clazz.newInstance(); Method setNameMethod = clazz.getDeclaredMethod("setName" ,String.class); setNameMethod.invoke(o,"erdai666" ); Method printNameMethod = clazz.getDeclaredMethod("printName" ); printNameMethod.invoke(o); } catch (Exception e) { e.printStackTrace(); } } } erdai666
3、通过定义一个新接口的方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 interface IPerson { void setName (String name) ; String getName () ; void printName () ; }public class TestCreateClass { public static void main (String[] args) { try { ClassPool classPool = ClassPool.getDefault(); classPool.appendClassPath ("/Users/zhouying/AndroidStudioProjects/MixDemo/GradleTransformDemo/app/src/main/java/" ); CtClass iPersonCtClass = classPool.get("com.dream.gradletransformdemo.IPerson" ); CtClass personCtClass = classPool.get("com.dream.gradletransformdemo.Person" ); 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 的文件,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 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 对它进行修改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 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.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(); Method personFlyMethod = clazz.getDeclaredMethod("personFly" ); personFlyMethod.invoke(o); Method joinFriendMethod = clazz.getDeclaredMethod("joinFriend" ); joinFriendMethod.invoke(o); } catch (Exception e) { e.printStackTrace(); } } } 起飞之前准备降落伞 I believe i can fly... 成功落地... I want to be your friend
需要注意 的是: 上面的insertBefore
,insertAfter
,setBody
中的语句,如果你是单行语句可以直接用双引号,但是有多行语句的情况下,你需要将多行语句用{}
括起来。Javassist 只接受单个语句或用大括号括起来的语句块
接下来我们进入实战环节
首先看下我们要解决的第一个问题:发现某个第三方库有 bug ,用起来不爽,但又不想拿它的源码修改在重新编译,有什么好的办法?
我的思路:使用 Gradle Transform + Javassit 修改库里面方法的内部实现 ,等于没说,😄,我们来实操一下
首先引入一个我准备好的第三方库:
在项目的根 build.gradle 加入 Jitpack 仓库:
1 2 3 4 5 6 allprojects { repositories { maven { url 'https://jitpack.io' } } }
在 app 的 build.gradle 中添加如下依赖:
1 implementation 'com.github.sweetying520:AndroidUtils:1.0.7'
ok,接着我们看下这个库中 StringUtils 的源码:
嗯,就两个简单的工具类,我们在 MainActivity 中使用一下:
1 2 3 4 5 6 7 8 9 10 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 进行插桩,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 class FixThirdLibTransform : BaseCustomTransform (true ) { override fun getName () : String { return "FixThirdLibTransform" } override fun classFilter (className: String ) = className.endsWith("StringUtils.class" ) @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) val getLengthMethod = makeClass.getDeclaredMethod("getLength" ) getLengthMethod.insertBefore("{System.out.println(\"Hello getLength bug修复了..\");if($1==null)return 0;}" ) val getCharArrayMethod = makeClass.getDeclaredMethod("getCharArray" ) getCharArrayMethod.insertBefore("{System.out.println(\"Hello getCharArray bug修复了..\");if($1==null)return new char[0];}" ) log("插桩的类名:${makeClass.name} " ) makeClass.declaredMethods.forEach { log("插桩的方法名:$it " ) } output.write(makeClass.toBytecode()) makeClass.detach() } catch (e: Exception) { e.printStackTrace() } } }
在 CustomTransformPlugin 进行插件的注册:
1 2 3 4 5 6 7 8 9 10 11 12 class CustomTransformPlugin : Plugin <Project > { override fun apply (project: Project ) { val androidExtension = project.extensions.getByType(AppExtension::class .java) androidExtension.registerTransform(FixThirdLibTransform()) } }
发布一个新的插件版本,修改根 build.gradle 插件的版本,同步后重新运行 app,效果验证:
1、先看一眼我们自定义 Transform 里面的 log 打印,符合预期:
2、在看下 app 效果,没有奔溃,符合预期
3、最后看一眼我们插桩的 log 日志,符合预期
接下来我们使用 Gradle Transform + ASM 解决第二个问题:我想在不修改源码的情况下,统计某个方法的耗时,对某个方法做埋点,怎么做?
就以 MainActivity 的 onCreate 方法为例子,我们统计一下 onCreate 方法的耗时
1、首先需要大家先安装一个插件:ASM Bytecode Viewer Support Kotlin ,这个插件能帮助我们快速的进行 ASM 字节码插桩的操作
打开 MainActivity ,看一眼 onCreate 方法插桩之前的代码:
1 2 3 4 5 6 7 8 9 10 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
会生成如下代码,选择 ASMified ,就可以看到 ASM 字节码了
注意 :我们要操作的是 ASM 字节码,而非 Java 字节码,其实二者非常接近,只不过 ASM 字节码是用 Java 代码的形式来描述的
2、修改 MainActivity onCreate 方法代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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 ,就会出来前后两次代码的对比,绿色部分代码就是我们要添加的
3、创建一个 Transform 继承 BaseTransform ,编写 ASM 代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 class CostTimeTransform : BaseCustomTransform (true ) { override fun getName () : String { return "CostTimeTransform" } override fun classFilter (className: String ) = className.endsWith("Activity.class" ) @Incubating override fun applyToVariant (variant: VariantInfo ?) : Boolean { return "debug" == variant?.buildTypeName } override fun provideFunction () = { input: InputStream,output: OutputStream -> val reader = ClassReader(input) val writer = ClassWriter(reader, ClassWriter.COMPUTE_FRAMES) val visitor = CostTimeClassVisitor(writer) reader.accept(visitor, ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES) val byteArray = writer.toByteArray() output.write(byteArray) } }
4、核心逻辑的处理是在我们自定义的 ClassVisitor 中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 package com.dream.customtransformpluginimport org.objectweb.asm.*import org.objectweb.asm.commons.AdviceAdapterclass 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) 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、在主工程中创建一个自定义注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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 代码进行修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 class CostTimeClassVisitor (nextVisitor: ClassVisitor) : ClassVisitor(Opcodes.ASM6,nextVisitor) { 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? { 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 进行插件的注册:
1 2 3 4 5 6 7 8 9 10 11 12 class CustomTransformPlugin : Plugin <Project > { override fun apply (project: Project ) { val androidExtension = project.extensions.getByType(AppExtension::class .java) androidExtension.registerTransform(CostTimeTransform()) } }
发布一个新的插件版本,修改根 build.gradle 插件的版本,同步后重新运行 app,效果验证:
1、看控制台 log 日志打印,符合预期:
2、接着通过反编译工具看下我们插桩后的 MainActivity ,符合预期:
Google 在 AGP 8.0 会将 Gradle Transform 给移除,因此如果项目升级了 AGP 8.0,就需要做好 Gradle Transform 的兼容。
Gradle Transform
被废弃之后,它的代替品是Transform Action
,Transform API
是由AGP
提供的,而Transform Action
则是由Gradle 提供。不光是 AGP
需要 Transform
,Java
也需要,所以由 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 即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.dream.customtransformplugin.foragp8import com.android.build.api.instrumentation.*import com.dream.customtransformplugin.costtime.CostTimeClassVisitorimport org.objectweb.asm.ClassVisitorabstract 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 进行注册,注册使用了一种新的方式:
3、发布一个新的插件版本,修改根 build.gradle 插件的版本,同步后重新运行 app,效果是一样的
一些不同点:
1、编译任务的 Task 名称变了:
2、我们编译生成的中间产物有了 Asm 相关的文件夹,方便我们一个效果的验证
需要注意的是 : 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 ,文章更新可第一时间收到
如果有问题 ,公众号内有加我微信的入口,在技术学习、个人成长的道路上,我们一起前进!