gradle自定義外掛

夕陽下的奔跑發表於2020-02-28
每個方法增加日誌輸出,記錄呼叫時間

使用Gradle自定義一個外掛,並且在程式碼編譯階段,使用ASM在Transform中進行程式碼插入。

一、gradle外掛

1. 建立module和groovy目錄

gradle自定義外掛

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,結果:

gradle自定義外掛

二、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()方法中

相關文章