持续创作,加快成长!这是我参加「日新方案 10 月更文挑战」的第6天,点击查看活动概况
前言
很高兴遇见你~
关于 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 长途依靠:
//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
办法,然后在调用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 办法。如下示例:
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
需求留意的是: 上面的insertBefore
,insertAfter
,setBody
中的句子,假如你是单行句子能够直接用双引号,可是有多行句子的情况下,你需求将多行句子用{}
括起来。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 的源码:
嗯,就两个简略的工具类,咱们在 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 打印,契合预期:
2、在看下 app 作用,没有奔溃,契合预期
3、最终看一眼咱们插桩的 log 日志,契合预期
四、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
会生成如下代码,挑选 ASMified ,就能够看到 ASM 字节码了
留意:咱们要操作的是 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 ,就会出来前后两次代码的比照,绿色部分代码便是咱们要增加的
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 日志打印,契合预期:
2、接着经过反编译工具看下咱们插桩后的 MainActivity ,契合预期:
五、AGP 8.0 版别关于 Gradle Transform 的替换方案
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 即可
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 进行注册,注册运用了一种新的办法:
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 ,文章更新可第一时间收到
假如有问题,大众号内有加我微信的入口,在技术学习、个人成长的道路上,咱们一同行进!