阅读 587

Android自定义Gradle插件入门

这次的demo是监控某个类的方法执行时间,作为学习Gradle插件开发的入门,因为我自己也是初学者,网上资料很多说的不全,所以自己总结一下,帮助自己记忆,也帮助别人学习。

主要分3步:

  1. 新建插件项目,编写插件项目的build.gradle文件
  2. 编写Plugin<Project>的子类
  3. 注册Transform的子类
  4. 因为要监控方法执行时间,所以需要将代码插入到方法头和方法尾,所以用到MethodVisitor
  5. 添加properties文件,如项目结构图所示
  6. 配置根目录的build.gradle文件
  7. 将插件应用到app module,编写app的build.gradle文件如下
  8. 编写app module代码,并注册MyApp到AndroidManifest.xml中
  9. 完成

先放完整项目结构图

image.png image.png

一、新建插件项目,编写插件项目的build.gradle文件

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

repositories {
    mavenCentral()
    jcenter()
}

dependencies {
    //gradle sdk
    implementation gradleApi()
    //groovy sdk
    implementation localGroovy()

    implementation 'com.android.tools.build:gradle:3.5.0'
    //ASM相关依赖
    implementation 'org.ow2.asm:asm:7.1'
    implementation 'org.ow2.asm:asm-commons:7.1'
}
// uploadArchives是将已经自定义好了插件打包到本地Maven库里面去了,
// 你也可以选择打包到远程服务器中。其中,
// group和version是我们之后配置插件地址时要用到的。
group='com.liuhc.asm.plugin'
version='1.2'
uploadArchives {
    repositories {
        mavenDeployer {
            //本地的Maven地址:当前工程下
            repository(url: uri('./plugin'))

            //提交到远程服务器:
            // repository(url: "http://www.xxx.com/repos") {
            //    authentication(userName: "admin", password: "admin")
            // }
        }
    }
}
复制代码

二、新建项目,编写Plugin<Project>的子类

package com.liuhc.plugin

import com.android.build.gradle.AppExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.jetbrains.annotations.NotNull

public class AsmPlugin implements Plugin<Project> {

    @Override
    public void apply(@NotNull Project project) {
        def android = project.extensions.getByType(AppExtension)
        println '----------- 开始注册 >>>>> -----------'
        AsmTransform transform = new AsmTransform()
        android.registerTransform(transform)
    }
}
复制代码

获取project中的AppExtension类型extension,然后注册我们自己定义的Transform。

啥是AppExtension,我们app的gradle中最上面都有这个插件apply plugin: 'com.android.application如果依赖了这个插件,AppExtension就存在。

三、注册Transform的子类

package com.liuhc.plugin

import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import com.liuhc.asm.LogVisitor
import org.apache.commons.codec.digest.DigestUtils
import org.apache.commons.io.FileUtils
import org.apache.commons.io.IOUtils
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.ClassWriter

import java.util.jar.JarEntry
import java.util.jar.JarFile
import java.util.jar.JarOutputStream
import java.util.zip.ZipEntry

public class AsmTransform extends Transform {

    // 设置我们自定义的Transform对应的Task名称
    // 编译的时候可以在控制台看到 比如:Task :app:transformClassesWithAsmTransformForDebug
    @Override
    public String getName() {
        return "AsmTransform"
    }
    // 指定输入的类型,通过这里的设定,可以指定我们要处理的文件类型
    // 这样确保其他类型的文件不会传入
    @Override
    public Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS;
    }
    // 指定Transform的作用范围
    @Override
    public Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    @Override
    public boolean isIncremental() {
        return false
    }

    @Override
    public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        long startTime = System.currentTimeMillis()
        println '----------- startTime <' + startTime + '> -----------'
        //拿到所有的class文件
        Collection<TransformInput> inputs = transformInvocation.inputs;
        TransformOutputProvider outputProvider = transformInvocation.outputProvider;
        if (outputProvider != null) {
            outputProvider.deleteAll()
        }
        //遍历inputs Transform的inputs有两种类型,一种是目录,一种是jar包,要分开遍历
        inputs.each { TransformInput input ->
            //遍历directoryInputs(文件夹中的class文件) directoryInputs代表着以源码方式参与项目编译的所有目录结构及其目录下的源码文件
            // 比如我们手写的类以及R.class、BuildConfig.class以及R$XXX.class等
            input.directoryInputs.each { DirectoryInput directoryInput ->
                //文件夹中的class文件
                handDirectoryInput(directoryInput, outputProvider)
            }
            //遍历jar包中的class文件 jarInputs代表以jar包方式参与项目编译的所有本地jar包或远程jar包
            input.jarInputs.each { JarInput jarInput ->
                //处理jar包中的class文件
                handJarInput(jarInput, outputProvider)
            }
        }
    }

    //遍历directoryInputs  得到对应的class  交给ASM处理
    private static void handDirectoryInput(DirectoryInput input, TransformOutputProvider outputProvider) {
        System.out.println("LogVisitor : handDirectoryInput ----->:");
        //是否是文件夹
        if (input.file.isDirectory()) {
            //列出目录所有文件(包含子文件夹,子文件夹内文件)
            input.file.eachFileRecurse { File file ->
                System.out.println("LogVisitor : file.name ----->:" + file.name);
                String name = file.name
                //需要插桩class 根据自己的需求来------------- 这里判断是否是我们自己写的Application
                if ("MyApp.class".equals(name)) {
                    ClassReader classReader = new ClassReader(file.bytes)
                    //传入COMPUTE_MAXS  ASM会自动计算本地变量表和操作数栈
                    ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
                    //创建类访问器   并交给它去处理
                    ClassVisitor classVisitor = new LogVisitor(classWriter)
                    classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)
                    byte[] code = classWriter.toByteArray()
                    FileOutputStream fos = new FileOutputStream(file.parentFile.absolutePath + File.separator + name)
                    fos.write(code)
                    fos.close()
                }
            }
        }
        //处理完输入文件后把输出传给下一个文件
        def dest = outputProvider.getContentLocation(input.name, input.contentTypes, input.scopes, Format.DIRECTORY)
        FileUtils.copyDirectory(input.file, dest)
    }
    //遍历jarInputs 得到对应的class 交给ASM处理
    private static void handJarInput(JarInput jarInput, TransformOutputProvider outputProvider) {
        if (jarInput.file.getAbsolutePath().endsWith(".jar")) {
            //重名名输出文件,因为可能同名,会覆盖
            def jarName = jarInput.name
            def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
            if (jarName.endsWith(".jar")) {
                jarName = jarName.substring(0, jarName.length() - 4)
            }
            JarFile jarFile = new JarFile(jarInput.file)
            Enumeration enumeration = jarFile.entries()
            File tmpFile = new File(jarInput.file.getParent() + File.separator + "classes_temp.jar")
            //避免上次的缓存被重复插入
            if (tmpFile.exists()) {
                tmpFile.delete()
            }
            JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(tmpFile))
            //用于保存
            while (enumeration.hasMoreElements()) {
                JarEntry jarEntry = (JarEntry) enumeration.nextElement()
                String entryName = jarEntry.getName()
                ZipEntry zipEntry = new ZipEntry(entryName)
                InputStream inputStream = jarFile.getInputStream(jarEntry)
                //需要插桩class 根据自己的需求来-------------
                if ("androidx/fragment/app/FragmentActivity.class".equals(entryName)) {
                    //class文件处理
                    println '----------- jar class  <' + entryName + '> -----------'
                    jarOutputStream.putNextEntry(zipEntry)
                    ClassReader classReader = new ClassReader(IOUtils.toByteArray(inputStream))
                    ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
                    //创建类访问器   并交给它去处理
                    ClassVisitor cv = new LogVisitor(classWriter)
                    classReader.accept(cv, ClassReader.EXPAND_FRAMES)
                    byte[] code = classWriter.toByteArray()
                    jarOutputStream.write(code)
                } else {
                    jarOutputStream.putNextEntry(zipEntry)
                    jarOutputStream.write(IOUtils.toByteArray(inputStream))
                }
                jarOutputStream.closeEntry()
            }
            //结束
            jarOutputStream.close()
            jarFile.close()
            //获取output目录
            def dest = outputProvider.getContentLocation(jarName + md5Name,
                    jarInput.contentTypes, jarInput.scopes, Format.JAR)
            FileUtils.copyFile(tmpFile, dest)
            tmpFile.delete()
        }
    }
}
复制代码

四、因为要监控方法执行时间,所以需要将代码插入到方法头和方法尾,所以用到MethodVisitorClassVisitor

因为我们要判断是某个具体的类型,所以需要用到ClassVisitor

package com.liuhc.asm;

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class LogVisitor extends ClassVisitor {
    private String mClassName;
    public LogVisitor(ClassVisitor classVisitor) {
        super(Opcodes.ASM5, classVisitor);
    }
    
    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        System.out.println("LogVisitor : visit -----> started:" + name);
        this.mClassName = name;
        super.visit(version, access, name, signature, superName, interfaces);
    }
    //定义一个方法, 返回的MethodVisitor用于生成方法相关的信息
    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        System.out.println("LogVisitor : this.mClassName ----> " + this.mClassName);
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
        if ("com/liuhc/testasm/MyApp".equals(this.mClassName)) {
            if ("onCreate".equals(name)) {
                //处理onCreate
                System.out.println("LogVisitor : visitMethod method ----> " + name);
                return new OnCreateVisitor(mv);
            }
        }
        return mv;
    }
    //访问结束
    @Override
    public void visitEnd() {
        System.out.println("LogVisitor : visit -----> end");
        super.visitEnd();
    }
}
复制代码
package com.liuhc.asm;

import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;


public class OnCreateVisitor extends MethodVisitor {

    public OnCreateVisitor(MethodVisitor methodVisitor) {
        super(Opcodes.ASM5, methodVisitor);
    }
    //开始访问方法
    @Override
    public void visitCode() {
        super.visitCode();

        mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
        mv.visitVarInsn(Opcodes.LSTORE, 1);
    }

    @Override
    public void visitInsn(int opcode) {
        //判断内部操作指令
        //当前指令是RETURN,表示方法内部的代码已经执行完
        if (opcode == Opcodes.RETURN) {
            mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitVarInsn(Opcodes.LLOAD, 1);
            mv.visitInsn(Opcodes.LSUB);
            mv.visitVarInsn(Opcodes.LSTORE, 3);
            Label l3 = new Label();
            mv.visitLabel(l3);
            mv.visitLineNumber(20, l3);
            mv.visitLdcInsn("TAG");
            mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
            mv.visitInsn(Opcodes.DUP);
            mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
            mv.visitLdcInsn("interval:");
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            mv.visitVarInsn(Opcodes.LLOAD, 3);
            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", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false);
            mv.visitInsn(Opcodes.POP);
        }
        super.visitInsn(opcode);
    }

    @Override
    public void visitEnd() {
        super.visitEnd();
        //访问结束
    }
}
复制代码

五、上传插件到本地库

执行gradle uploadArchives命令,将插件上传到本地库。

六、配置根目录的build.gradle文件

// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
    ext.kotlin_version = "1.5.0"
    repositories {
        google()
        jcenter()
        maven {
            //本地仓库地址
            url uri('./myPlugin/plugin')
        }
    }
    dependencies {
        classpath "com.android.tools.build:gradle:4.2.1"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        //自己写的插件,注意,这里的名字,网上基本没有人说过这部分具体怎么写,其中com.liuhc.asm.plugin是之前build.gradle中配置的group的名字,而myPlugin是groovy文件所在module的名字,1.2是之前build.gradle中配置的version的名字
        classpath 'com.liuhc.asm.plugin:myPlugin:1.2'
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        google()
        mavenCentral()
        jcenter() // Warning: this repository is going to shut down soon
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}
复制代码

七、将插件应用到app module,编写app的build.gradle文件如下

plugins {
    id 'com.android.application'
    id 'kotlin-android'
    //这里的com.liuhc.plugin是之前创建的properties文件去除properties文件后缀以后前面的名字
    id 'com.liuhc.plugin'
}

android {
    compileSdkVersion 30

    defaultConfig {
        applicationId "com.liuhc.testasm"
        minSdkVersion 21
        targetSdkVersion 30
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
}

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.3.1'
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'com.google.android.material:material:1.3.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}
复制代码

八、编写app module代码,并注册MyApp到AndroidManifest.xml中

package com.liuhc.testasm

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}
复制代码
package com.liuhc.testasm

import android.app.Application

/**
 * 描述:
 * 作者:liuhc
 * 创建日期:2021/5/19 on 5:07 下午
 */
class MyApp : Application() {

    override fun onCreate() {
        super.onCreate()
    }
}
复制代码
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.liuhc.testasm">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:name=".MyApp"
        android:theme="@style/Theme.TestASM">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>
复制代码

九、完成

这就完成了,代码没整理,但是就是这个流程,其中System.out.print输出的文字,在Android Studio的这个图标的打印里能看到 ,而如果想在LogCat里看到输出需要用到MethodVisitor#visitLdcInsn方法,上文代码中有示例,当然我们没必要写这种OnCreateVisitor#visitInsn里面这种不容易编写的代码,我们可以使用 这个插件,但是这个插件只能在IDEA里使用,所以需要再装一个IDEA,具体使用方法不再赘述,可以自己谷歌一下


参考:chsmy.github.io/2019/09/28/…

www.jianshu.com/p/16ed4d233…

文章分类
Android
文章标签