ASM和自定义Transform的应用实践

上篇文章学习了下如何自定义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项目)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章