上篇文章学习了下如何自定义gradle插件《自定义gradle》,在实际应用中一般都会结合自定义的Transform和字节码操作框架(像ASM、Javassist)通过操作字节码来实现自己的业务逻辑,Transform是Android Gradle 在1.5.0 版本后提供的, 它允许第三方的Plugin插件在打包成 dex 文件之前的编译过程中操作 class 文件,对于一些重复性的操作,程序员可以统一修改class文件来实现自己的功能,从而避免在项目中出现大量的重复代码。在Android项目中常用的场景有统计打点、打印方法执行的耗时时长等,本文中以统计打印Activity的打开和关闭事件为例,来学习实践下整个过程。
1、创建自定义的Plugin插件
在custom_plugin的src/main/groovy/com/znh/plugin下创建一个用于页面统计的自定义插件PagePlugin,代码如下:
package com.znh.plugin
import com.android.build.gradle.AppExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
/**
* Created by znh on 2020-04-21
* <p>
* 用于页面统计的Plugin
*/
class PagePlugin implements Plugin<Project> {
@Override
void apply(Project project) {
//注册自定义的PageTransform
def androidConfig = project.extensions.getByType(AppExtension)
PageTransform transform = new PageTransform()
androidConfig.registerTransform(transform)
}
}
注:不要忘记在resources/META-INF/gradle-plugins下为PagePlugin配置对应的properties文件
2、引入ASM依赖并创建Transform
这里选用的操作字节码的框架是ASM,所以需要引入ASM的依赖包,由于Transform是android gradle下面的api,所以也需要引入android gradle环境,在custom_plugin的build.gradle文件中添加以上配置,配置代码如下:
//引入android环境
implementation 'com.android.tools.build:gradle:3.6.3'
//ASM依赖
implementation 'org.ow2.asm:asm:7.1'
implementation 'org.ow2.asm:asm-commons:7.1'
在custom_plugin的src/main/groovy/com/znh/plugin下创建一个用于页面统计的Transform类PageTransform,代码如下:
package com.znh.plugin
import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import com.znh.gradle.custom.plugin.PageClassVisitor
import groovy.io.FileType
import org.apache.commons.io.FileUtils
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.ClassWriter
/**
* Created by znh on 2020-04-21
* <p>
* 处理页面统计的Transform
*/
class PageTransform extends Transform {
@Override
String getName() {
return "PageTransform"
}
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
@Override
Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
@Override
boolean isIncremental() {
return false
}
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation)
Collection<TransformInput> inputs = transformInvocation.inputs
TransformOutputProvider outputProvider = transformInvocation.outputProvider
inputs.each {
TransformInput transformInput ->
//Gradle3.6.0之后需要单独复制jar包
transformInput.jarInputs.each { JarInput jarInput ->
File jarFile = jarInput.file
def destJar = outputProvider.getContentLocation(jarInput.name,
jarInput.contentTypes,
jarInput.scopes, Format.JAR)
FileUtils.copyFile(jarFile, destJar)
}
//操作class文件
transformInput.directoryInputs.each {
DirectoryInput directoryInput ->
File dir = directoryInput.file
if (dir) {
dir.traverse(type: FileType.FILES, nameFilter: ~/.*\.class/) { File file ->
//读取和解析class文件
ClassReader classReader = new ClassReader(file.bytes)
//对class文件的写入
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
//访问class文件的内容
ClassVisitor classVisitor = new PageClassVisitor(classWriter)
//调用classVisitor的各个方法
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)
//将修改的字节码以byte数组返回
byte[] bytes = classWriter.toByteArray()
//通过文件流写入方式覆盖原先的内容,完成class文件的修改
FileOutputStream outputStream = new FileOutputStream(file.path)
outputStream.write(bytes)
outputStream.close()
}
}
//处理完输入文件后,把输出传递给下一个transform
def destDir = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
FileUtils.copyDirectory(directoryInput.file, destDir)
}
}
}
}
- getName方法:执行这个Transform的task时,会以这个名字为基础生成task名称( 比如这里的任务是Task :app:transformClassesWithPageTransformForDebug)
- getInputTypes方法:表示要处理的数据类型是什么,CLASSES 表示要处理编译后的字节码(可能是 jar 包也可能是目录),RESOURCES 表示要处理的是标准的 java 资源
- getScopes方法:表示Transform 的作用域,这里设置的SCOPE_FULL_PROJECT代表作用域是全工程
- isIncremental方法:表示是否支持增量编译,false不支持
- transform方法:在这个方法中获取到文件输入源,对里面的class文件做一些自定义的修改后,将文件复制到指定目录中做为下一个Transform的输入源,多个Transfrom之间是一条执行链,上一个Transfrom的输出作为下一个Transfrom的输入,由于transform方法中的父类是空实现,所以当自定义一个Transfrom时,就算对class不进行任何操作,在这个方法中仍然要将文件复制到指定目录作为下一个Transfrom的输入,如果不做复制操作直接调用super方法,那整个执行链将断开而导致程序异常
上述代码中的PageClassVisitor类是自定义的操作class文件的类,它实现了ASM框架中的ClassVisitor类
3、基于ASM创建class文件的操作类
需要根据ASM框架提供的api和规则来创建操作字节码文件的操作类,这里需要修改字节码来实现自己的业务逻辑,所以为了方便这里使用java类来实现,在/custom_plugin的src/main/java/com/znh/gradle/custom/plugin下创建PageClassVisitor和PageMethodVisitor用来修改class字节码,插入自己的统计指令,以完成自己的业务需求。PageClassVisitor类用于对class字节码的观察并监听类的信息,PageClassVisitor代码如下:
package com.znh.gradle.custom.plugin;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
/**
* Created by znh on 2020-04-21
* <p>
* ASM操作class的类
*/
public class PageClassVisitor extends ClassVisitor {
//当前类的类名称
//本例:com/znh/gradle/plugin/demo/MainActivity
private String className;
//className类的父类名称
//本例:androidx/appcompat/app/AppCompatActivity
private String superName;
public PageClassVisitor(ClassVisitor classVisitor) {
super(Opcodes.ASM5, classVisitor);
}
@Override
public void visit(int version, int access, String className, String signature, String superName, String[] interfaces) {
super.visit(version, access, className, signature, superName, interfaces);
this.className = className;
this.superName = superName;
}
@Override
public MethodVisitor visitMethod(int access, String methodName, String descriptor, String signature, String[] exceptions) {
MethodVisitor methodVisitor = cv.visitMethod(access, methodName, descriptor, signature, exceptions);
//判断是否自己要匹配的方法(这里匹配onCreate和onDestroy方法)
if ("android/support/v7/app/AppCompatActivity".equals(superName)//support包
|| "androidx/appcompat/app/AppCompatActivity".equals(superName)) {//androidx包
if (methodName.startsWith("onCreate")) {
return new PageMethodVisitor(methodVisitor, className, methodName);
}
if (methodName.startsWith("onDestroy")) {
return new PageMethodVisitor(methodVisitor, className, methodName);
}
}
return methodVisitor;
}
}
PageMethodVisitor类用于对方法的观察,可以在visitCode的方法中修改字节码,插入自己的字节码指令,从而植入自己的业务逻辑,PageMethodVisitor类代码如下:
package com.znh.gradle.custom.plugin;
import org.objectweb.asm.MethodVisitor;
import groovyjarjarasm.asm.Opcodes;
/**
* Created by znh on 2020-04-21
* <p>
* ASM操作Method方法的类
*/
public class PageMethodVisitor extends MethodVisitor {
//当前的类名
private String className;
//当前的方法名
private String methodName;
PageMethodVisitor(MethodVisitor methodVisitor, String className, String methodName) {
super(Opcodes.ASM5, methodVisitor);
this.className = className;
this.methodName = methodName;
}
@Override
public void visitCode() {
super.visitCode();
//在这里插入自己的字节码指令
//可以插入页面打开关闭的打点上报代码,这里用log打印代替了
mv.visitLdcInsn("Page_TAG");//可以用来过滤log日志的tag
mv.visitLdcInsn(className + "--->" + methodName);//插入要打印的内容
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false);
mv.visitInsn(Opcodes.POP);
}
}
4、测试打印结果
上述代码编写完成后,需要将插件重新上传到maven上,然后在app的build.gradle中引入新上传的插件,运行项目并查看log日志如下:
项目地址:https://github.com/huihuigithub/blog_demo_projects.git(gradle_plugin_demo项目)