gradle自定义插件

693 阅读4分钟
每个方法增加日志输出,记录调用时间

使用Gradle自定义一个插件,并且在代码编译阶段,使用ASM在Transform中进行代码插入。

一、gradle插件

1. 创建module和groovy目录

2. 定义build.gradle

apply plugin: 'groovy'
apply plugin: 'maven'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    //gradle和groovy的依赖
    implementation gradleApi()
    implementation localGroovy()
}

dependencies {
    //直接使用build工具的asm类库,不再额外引入asm的依赖
    implementation 'com.android.tools.build:gradle:3.5.3'
}

repositories {
    mavenCentral()
}
//本地配置的参数,需要上传maven时配置用户名和密码等信息
Properties props = new Properties()
props.load(project.rootProject.file('local.properties').newDataInputStream())
def artifactory_user = props.getProperty('user')
def pwd = props.getProperty('password')
def contextUrl = props.getProperty('contextUrl')
def repoKey = props.getProperty('repoKey')
def releaseRepoKey = props.getProperty('release_repoKey')

def snapshot = true
def log_version = '1.0.0'
def log_group = 'com.example.autolog'
def log_id = 'auto-log'

group = log_group
version = log_version

def debug = true

uploadArchives {
    repositories {
        mavenDeployer {
            if (debug) {
                //本地仓库,生成的jar和pom放在根目录下的repo-local目录中
                repository(url: "file://$projectDir/../repo-local")
            } else {
                def repokey = snapshot ? repoKey : releaseRepoKey
                repository(url: "${contextUrl}/${repokey}") {
                    authentication(username: artifactory_user, password: pwd)
                }
            }
            pom.groupId = log_group
            pom.artifactId = log_id
            pom.version = log_version

            pom.project {
                licenses {
                    license {
                        name 'The Apache Software License, Version 2.0'
                        url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
                    }
                }
            }
        }
    }
}

3. 创建Plugin

AutoLogPlugin.groovy:

package com.example.autolog

import com.android.build.gradle.AppExtension
import com.android.build.gradle.AppPlugin
import org.gradle.api.Plugin
import org.gradle.api.Project
//实现Plugin接口
public class AutoLogPlugin implements Plugin<Project> {
    @Override
    void apply(Project project) {
        //判断项目使用了com.android.application插件
        def isApp = project.plugins.hasPlugin(AppPlugin)
        if(isApp){
            println 'project('+project.name+') apply auto-log plugin'
            def android = project.extensions.getByType(AppExtension)
            def transform = new LogTransform(project)
            //注册Transform
            android.registerTransform(transform)
            project.afterEvaluate {
                //do init
            }
        }
    }
}

4. 创建LogTransform

package com.example.autolog

import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import org.gradle.api.Project
import org.objectweb.asm.*
import org.objectweb.asm.util.TraceClassVisitor

public class LogTransform extends Transform {
    Project project;

    LogTransform(Project project) {
        this.project = project
    }

    @Override
    String getName() {
        return 'auto-log'
    }
	//处理类型:java字节码会被处理
    @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 {
        project.logger.warn('start auto-log transform..')
        if (!transformInvocation.incremental) {
            transformInvocation.outputProvider.deleteAll()
        }

        transformInvocation.inputs.each { TransformInput input ->
            input.jarInputs.each { JarInput jarInput ->
                //遍历jar文件
                scanJar(jarInput, transformInvocation.outputProvider)
            }
            input.directoryInputs.each { DirectoryInput directoryInput ->
                //遍历源码目录
                project.logger.warn("directoryInput : " + directoryInput.name)
                directoryInput.file.eachFileRecurse { File file ->
                    project.logger.warn("directory file:"+file.name)
                    if (file.isFile()) {
                        //如果是字节码文件,进行处理
                        scanClass(file)
                    }
                }
            }
        }
    }

    void scanJar(JarInput jarInput, TransformOutputProvider outputFileProvider) {

    }

    void scanClass(File file) {
        project.logger.warn("scanClass " + file.name)

        def optclass = new File(file.getParent(), file.name + ".opt")
        FileInputStream is = new FileInputStream(file)
        FileOutputStream os = new FileOutputStream(optclass)
		//返回处理后的字节码,写入文件
        def bytes = generateCode(is,file.name)

        os.write(bytes)
        is.close()
        os.close()
		//删除原有的编译后的class,将新生成的class重命名
        if (file.exists()) {
            file.delete()
        }
        optclass.renameTo(file)
    }

    byte[] generateCode(InputStream is, String name) {
        ClassReader cr = new ClassReader(is)
        ClassWriter cw = new ClassWriter(cr, 0)
        //ASM进行方法扫描的类
        ClassVisitor cv = new LogClassVisitor(Opcodes.ASM5, cw, name)
        //TraceClassVisitor可以将扫描过的类的字节码保存到本地
        TraceClassVisitor tcv = new TraceClassVisitor(cv, new PrintWriter(new File("E:/ASM/" + name + ".txt")))
        cr.accept(tcv, ClassReader.EXPAND_FRAMES)
        return cw.toByteArray()
    }

    class LogClassVisitor extends ClassVisitor {
        String cn
        boolean isInterface

        LogClassVisitor(int api) {
            super(api)
        }

        LogClassVisitor(int api, ClassVisitor cv, String className) {
            super(api, cv)
            this.cn = className
        }

        @Override
        void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
            super.visit(version, access, name, signature, superName, interfaces)
            //类是否是接口
            isInterface = access == Opcodes.ACC_INTERFACE
        }

        @Override
        MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
            MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions)
            //如果不是构造函数,且不是接口,则进行方法处理
            if(mv != null && name !="<init>" && !isInterface){
                return new LogMethodVisitor(Opcodes.ASM5, mv, name, cn)
            }
            return mv
        }
    }

    class LogMethodVisitor extends MethodVisitor {
        String mn
        String cn

        LogMethodVisitor(int api) {
            super(api)
        }

        LogMethodVisitor(int api, MethodVisitor mv, String methodName, String className) {
            super(api, mv)
            this.mn = methodName
            this.cn = className
        }

        @Override
        void visitCode() {
            //使用ASM接口进行方法注入
            mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false)
            mv.visitVarInsn(Opcodes.LSTORE, 1)
            mv.visitLdcInsn(cn)
            mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder")
            mv.visitInsn(Opcodes.DUP)
            mv.visitMethodInsn(Opcodes.INVOKESPECIAL,"java/lang/StringBuilder","<init>","()V", false)
            mv.visitLdcInsn(mn + " start")
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,"java/lang/StringBuilder","append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false)
            mv.visitVarInsn(Opcodes.LLOAD, 1)
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false)
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;",false)
            mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "v", "(Ljava/lang/String;Ljava/lang/String;)I", false)
            mv.visitInsn(Opcodes.POP)
            super.visitCode()
        }

        @Override
        void visitMaxs(int maxStack, int maxLocals) {
            //帧的最大栈长度,和本地变量的数量,可以查看字节码进行比对
            super.visitMaxs(maxStack + 4, maxLocals + 2)
        }
    }
}

5. 运行uploadArchives的task,结果:

二、ASM工具使用

1. 如何查看字节码

  1. Android Studio有一个插件——ASM Bytecode Outline,可以用来看类的字节码。

    (未运行成功,可能跟kotlin有关...)

  2. 使用ASM提供的TraceClassVisitor,将类的字节码保存到本地,然后再分析。

2. 使用ASM提供的API进行方法修改

  1. 未添加代码的方法:

    package com.example.autolog;
    
    public class Test {
    
       void test(){
       }
    }
    
  2. 使用TraceClassVisitor将对应的字节码保存到对应的文件,如下所示:

    // class version 51.0 (51)
    // access flags 0x21
    public class com/example/autolog/Test {
    
      // compiled from: Test.java
    
      // access flags 0x1
      public <init>()V
       L0
        LINENUMBER 3 L0
        ALOAD 0
        INVOKESPECIAL java/lang/Object.<init> ()V
        RETURN
       L1
        LOCALVARIABLE this Lcom/example/autolog/Test; L0 L1 0
        MAXSTACK = 1
        MAXLOCALS = 1
    
      // access flags 0x0
      test()V
       L0
        LINENUMBER 6 L0
        RETURN
       L1
        LOCALVARIABLE this Lcom/example/autolog/Test; L0 L1 0
        MAXSTACK = 0
        MAXLOCALS = 1
    }
    
    
  3. 添加代码的方法:

    package com.example.autolog;
    
    public class Test {
        void test(){
        }
    }
    
  4. 使用TraceClassVisitor将对应的字节码保存到对应的文件,如下所示:

    // class version 51.0 (51)
    // access flags 0x21
    public class com/example/autolog/Test {
    
      // compiled from: Test.java
    
      // access flags 0x1
      public <init>()V
       L0
        LINENUMBER 5 L0
        ALOAD 0
        INVOKESPECIAL java/lang/Object.<init> ()V
        RETURN
       L1
        LOCALVARIABLE this Lcom/example/autolog/Test; L0 L1 0
        MAXSTACK = 1
        MAXLOCALS = 1
    
      // access flags 0x0
      test()V
       L0
        LINENUMBER 8 L0
        INVOKESTATIC java/lang/System.currentTimeMillis ()J
        LSTORE 1
       L1
        LINENUMBER 9 L1
        LDC "Test"
        NEW java/lang/StringBuilder
        DUP
        INVOKESPECIAL java/lang/StringBuilder.<init> ()V
        LDC "test start:"
        INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
        LLOAD 1
        INVOKEVIRTUAL java/lang/StringBuilder.append (J)Ljava/lang/StringBuilder;
        INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
        INVOKESTATIC android/util/Log.v (Ljava/lang/String;Ljava/lang/String;)I
        POP
       L2
        LINENUMBER 10 L2
        RETURN
       L3
        LOCALVARIABLE this Lcom/example/autolog/Test; L0 L3 0
        LOCALVARIABLE start J L1 L3 1
        MAXSTACK = 4
        MAXLOCALS = 3
    }
    
  5. 将字节码中test()中多出来的内容用ASM的api添加即可,在LogMethodVisitor类的visitCode()方法中