自定义Gradle插件

可使用任何您喜欢的语言来实现Gradle插件,前提是该实现源码最终被编译为JVM字节码html

Gradle 脑图

  • 通过前面文章,是时候来个脑图总结一下

Gradle知识体系

自定义插件

  • 自定义插件有三种方式,分别为build.gradle中编写、buildSrc工程项目中编写(统一依赖管理)和插件以独立项目编译提供jar包形式引入项目

build.gradle 自定义插件

  • 要建立 Gradle 插件,您须要编写一个实现 Plugin 接口。将插件应用于项目时,Gradle 将建立插件类的实例,并调用该实例的 Plugin.apply()方法。项目对象做为参数传递,插件可使用它来配置项目。下面的示例包含一个Greeting插件,该插件将一个 hello 任务(task)添加到项目中。
  • 直接在 build.gradle 添加以下代码
class GreetingPlugin implements Plugin<Project>{

    @Override
    void apply(Project target) {

        target.task("hello"){
            doLast {
                println("Hello from the GreetingPlugin")
            }
        }
    }
}

apply plugin: GreetingPlugin
复制代码
  • 运行 gradle 命令(注意本地gradle版本与AS 版本想匹配,不然会编译失败)
gradle -q hello

输出 Hello from the GreetingPlugin
复制代码

build.gradle 自定义插件配置扩展

  • 为构建插件提供了一些配置选项,插件使用扩展对象执行此操做。Gradle 项目具备一个关联的ExtensionContainer对象,该对象包含已应用于该项目的插件的全部设置和属性。您能够经过向该容器添加扩展对象来为您的插件提供配置。扩展对象只是具备表示配置的Java Bean属性的对象
//build.gradle   自定义插件
class GreetingPlugin2 implements Plugin<Project> {
    void apply(Project project) {
        //获取配置
        def extension = project.extensions.create('greeting', GreetingPluginExtension)

        project.task('hello2') { //名字为 hello 的task
            doLast {
                //获取 extension 配置信息
                println "${extension.message} from ${extension.greeter}"
            }
        }
    }
}

//引入插件
apply plugin: GreetingPlugin2

// 配置 extension
greeting{
    greeter = 'Gradle'
    message = "Hi"
}
复制代码
  • 运行 gradle 命令
gradle -q hello2

输出 > Task :app:hello2
Hi from Gradle
复制代码
  • 以上分别建立了两个名为包含 hello 和 hello2 任务(task)的自定义插件,在 Tasks列表的 other 中也可找到

hello和hello2task

buildSrc工程项目自定义插件

  • 新建 buildSrc module,删除没必要要文件以下所示,并新建 groovy目录添加自定义插件 TestPlugin,同时也简单设置一个叫TestPlugin的task打印日志

注意:记得删除 settings.gradle buildSrc配置,不然会报'buildSrc' cannot be used as a project name as it is a reserved name 错误java

buildSrc自定义插件

import org.gradle.api.Plugin
import org.gradle.api.Project

class TestPlugin implements Plugin<Project>{

    @Override
    void apply(Project project) {

        println("====== buildSrc TestPlugin Plugin加载===========")
        //执行自定义的  task
        project.task("TestPlugin"){
            doLast {
                println("buildSrc TestPlugin task 任务执行")
            }
        }
    }
}
复制代码
  • 而后在 app 项目下 build.gradle 引入buildSrc 刚刚建立好的插件
apply plugin: TestPlugin
复制代码
  • 一样能够在 Task other 下找到 TestPlugin 执行

Taskother下找到TestPlugin

自定义插件编译成 jar 包

  • AS 中新建 module,删除其余文件,只保留 src 目和 build.gradle 脚本文件,以下图所示

自定义插件新建项目

  • 删除原有 build.gradle 脚本文件内容,修改成以下
## 须要引入的插件
apply plugin: 'groovy'
apply plugin: 'maven'

//gradle 开发 sdk 依赖
dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])

    implementation gradleApi()
    implementation localGroovy()

    implementation 'com.android.tools.build:gradle:4.0.0'

}

//设置插件 group 和版本号 在项目中使用的时候会用到
group='com.maoasm.plugin'
version='1.0.0'

uploadArchives {
    repositories {
        mavenDeployer {
            //本地的Maven地址设置
            repository(url: uri('../asm_test_repo'))
        }
    }
}
复制代码
  • 而后 在 mian 目录下新建 groovy 目录,由于gradle 是groovy写的,因此该目录用来存放插件相关的.groovy类,而后咱们建立 MainPlugin.groovy 文件,并实现插件接口,project 则表明引入插件的项目
package com.maoasm.plugin

import org.gradle.api.Plugin
import org.gradle.api.Project

class MainPlugin implements Plugin<Project>{

    @Override
    void apply(Project project) {

        println("======自定义MainPlugin加载===========")
        //执行自定义的  task
        project.task("TestPluginTask"){
            doLast {
                println("自定义插件task 任务执行")
            }
        }
    }
}
复制代码
  • 最后建立 properties 文件,maven 项目都须要这个配置,properties 文件名标识项目名称
## 本文件名称就是插件 apply 名称
implementation-class=com.maoasm.plugin.MainPlugin
复制代码

建立项目文件

注意,上图中的目录层级须要一一对应建立目录,保证父子目录,不然 apply 插件会出现如下错误android

Plugin with id 'XXXXX' not found
复制代码

插件上传到本地 maven 仓库

  • 前面在插件 build.gradle 脚本文件中咱们配置了上传 jar 的 uploadArchives task 任务,找到 gradle task 执行上传 jar 包,以下图为执行任务和上传 jar 成功

执行uploadArchives任务

自定义插件 jar 上传本地仓库成功

测试自定义插件

  • 在app项目的 build.gradle 引入咱们刚刚写好的插件
//引入自定义插件
apply plugin: 'com.mao.asmtest'
buildscript {
    repositories {
        google()
        jcenter()
        //自定义插件maven地址,这里以本地目录做为仓库地址目录
        maven { url '../asm_test_repo' }
    }
    dependencies {
        //加载自定义插件 group + module + version
        classpath 'com.maoasm.plugin:asm_test_plugin:1.0.0'
    }
}
复制代码

引入自定义插件

  • 插件引入编译成功,说明此时自定义插件加载成功

插件引入编译成功

  • 在项目Task 选项中执行这个自定义 TestPluginTask

TestPluginTask执行成功

实践:自定义插件实现Activity生命周期方法插桩打印log

  • 前面例子都是使用 grrovy 语法来编写自定义插件,开头已经说过,只有编写插件语言能编译成字节码就行,因此接下来使用 kotlin 来实现一个自定义插件,实现Activity生命周期方法插桩打印log。
  • 首先要改写插件的 build.gradle 引入依赖让插件可以编译 kotlin,以下所示加入kotlin依赖
apply plugin: 'groovy'
apply plugin: 'maven'
apply plugin: 'kotlin'

dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])

    implementation gradleApi()
    implementation localGroovy()

    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'com.android.tools.build:gradle:4.0.0'
}


group='com.mao.asm'
version='1.0.1'

uploadArchives {
    repositories {
        mavenDeployer {
            //本地的Maven地址设置
            repository(url: uri('../asm_test_repo'))
        }
    }
}
复制代码

Transform是什么?

  • 前面咱们写的代码在编译的时候可使用 project.task 来指定编译过程要作什么操做,要完成自动插桩,首先就要找到项目中对应的.class文件修改,编译过程当中 compileJava 这个task 将Java文件变成 .class ,若是编写一个 Transform 注册后 gradle 会将其看作是一个Task,并在 compileJava task 以后执行,Transform 接收这些 class 文件在执行插桩这就是这个自定义插件实现思路。git

  • 详情请看个人另外一篇文章深刻了解 Gradlegithub

使用 Transform Task 处理字节码文件

  • 在前面自定义插件编译基础上新建一个 kotlin 目录,这样插件才会编译 kotlin 目录代码,新建 MainPlugin.kt 类继承 Plugin 接口,泛型定义为加载插件的 Project
/**
 * @Description:
 * @author maoqitian
 * @date 2020/11/13 0013 17:01
 */
class MainPlugin :Plugin<Project> {
    override fun apply(project: Project) {
        println("======自定义MainPlugin加载===========")
    }
}
复制代码
  • 而后定义 ASMLifecycleTransform.kt 类 处理字节码文件,分别获取输入输出文件集合,遍历获得.class 文件,其中涉及 ASM 框架的 ClassReader和 ClassWriter下面会介绍。
/**
 * @Description: Transform 能够被看做是 Gradle 在编译项目时的一个 task
 * @author maoqitian
 * @date 2020/11/13 0013 17:03
 */
class ASMLifecycleTransform :Transform() {

    /**
     * 设置咱们自定义的 Transform 对应的 Task 名称。Gradle 在编译的时候,会将这个名称显示在控制台上
     * @return String
     */
    override fun getName(): String = "ASMLifecycleTransform111"

    /**
     * 在项目中会有各类各样格式的文件,该方法能够设置 Transform 接收的文件类型
     * 具体取值范围
     * CONTENT_CLASS  .class 文件
     * CONTENT_JARS  jar 包
     * CONTENT_RESOURCES  资源 包含 java 文件
     * CONTENT_NATIVE_LIBS native lib
     * CONTENT_DEX dex 文件
     * CONTENT_DEX_WITH_RESOURCES  dex 文件
     * @return
     */
    override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> = TransformManager.CONTENT_CLASS


    /**
     * 定义 Transform 检索的范围
     * PROJECT 只检索项目内容
     * SUB_PROJECTS 只检索子项目内容
     * EXTERNAL_LIBRARIES 只有外部库
     * TESTED_CODE 由当前变量测试的代码,包括依赖项
     * PROVIDED_ONLY 仅提供的本地或远程依赖项
     * @return
     */
    //只检索项目内容
    override fun getScopes(): MutableSet<in QualifiedContent.Scope> = TransformManager.PROJECT_ONLY

    /**
     * 表示当前 Transform 是否支持增量编译 返回 true 标识支持 目前测试插件不须要
     * @return Boolean
     */
    override fun isIncremental(): Boolean = false
    //对项目 class 检索操做
    override fun transform(transformInvocation: TransformInvocation) {
        println("transform 方法调用")

        //获取全部 输入 文件集合
        val transformInputs = transformInvocation.inputs
        val transformOutputProvider = transformInvocation.outputProvider

        transformOutputProvider?.deleteAll()

        transformInputs.forEach { transformInput ->
            // Caused by: java.lang.ClassNotFoundException: Didn't find class "androidx.appcompat.R$drawable" on path 问题
            // gradle 3.6.0以上R类不会转为.class文件而会转成jar,所以在Transform实现中须要单独拷贝,TransformInvocation.inputs.jarInputs
            // jar 文件处理
            transformInput.jarInputs.forEach { jarInput ->
                val file = jarInput.file
                println("find jar input:$file.name")
                val dest = transformOutputProvider.getContentLocation(jarInput.name, jarInput.contentTypes, jarInput.scopes, Format.JAR)
                FileUtils.copyFile(file, dest)
            }
            //源码文件处理
            //directoryInputs表明着以源码方式参与项目编译的全部目录结构及其目录下的源码文件
            transformInput.directoryInputs.forEach { directoryInput ->
                //遍历全部文件和文件夹 找到 class 结尾文件
                directoryInput.file.walkTopDown()
                    .filter { it.isFile }
                    .filter { it.extension == "class" }
                    .forEach { file ->
                        println("find class file:${file.name}")
                        val classReader = ClassReader(file.readBytes())
                        val classWriter = ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
                        //字节码插桩处理
                        //2.class 读取传入 ASM visitor
                        val asmLifecycleClassVisitor = ASMLifecycleClassVisitor(classWriter)
                        //3.经过ClassVisitor api 处理
                        classReader.accept(asmLifecycleClassVisitor,ClassReader.EXPAND_FRAMES)
                        //4.处理修改为功的字节码
                        val bytes = classWriter.toByteArray()
                        //写回文件中
                        val fos =  FileOutputStream(file.path)
                        fos.write(bytes)
                        fos.close()
                }
                //复制到对应目录
                val dest = transformOutputProvider.getContentLocation(directoryInput.name,directoryInput.contentTypes,directoryInput.scopes, Format.DIRECTORY)
                FileUtils.copyDirectory(directoryInput.file,dest)
            }
        }
    }
}
复制代码

ASM 字节码操做

  • 字节码操做已有现成的框架ASM,官方文档后端

  • 在插件 build.gradle 中依赖引入api

implementation 'org.ow2.asm:asm:9.0'
implementation 'org.ow2.asm:asm-commons:9.0'
复制代码

ASM 几个关键类

  • ClassReader:读取字节码文件的字节数组,并将字节码传递给ClassWriter
  • ClassWriter:它的父类是ClassVisitor,做用是生成修改后的字节码,并输出字节数组;字节码文件由无符号数和表组成,最终其实为十六进制数,在 ASM 修改了字节码文件以后,确定会影响到常量池的大小,此外包括本地变量表和操做数栈等变化,不过放心,只要在实例化 ClassWriter 操做类的时候设置 COMPUTE_MAXS 后,ASM 就会自动计算本地变量表和操做数栈。
val classWriter = ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
复制代码
  • ClassVisitor:用来解析字节码文件结构的,当解析到某些特定结构时(好比类、方法、变量、),则调用内部相应的 FieldVisitor 或者 MethodVisitor 的方法,进一步解析或者能够修改 字节码文件的内容帮助完成字节码插桩功能数组

  • 更多字节码结果组成内容可看个人另外一篇文章 从新认识Java字节码markdown

其余插桩框架

  • AspectJ,作事后端开发应该对其不陌生。它在 Android 中使用比较难搞,Android 可使用 AspectJX
  • facebook 的 redex , 它有提供在全部方法或者指定方法前面插入一段跟踪代码,具体我也没研究过,能够自行查看项目例子InstrumentTest.config

ASM 插桩实现

  • 使用 ClassVisitor 读取目标 Activity 的 .class 文件,并过滤对应生命周期方法
/**
 * @Description: class Visitor
 * @author maoqitian
 * @date 2020/11/13 0013 11:47
 */
class ASMLifecycleClassVisitor(classVisitor: ClassVisitor?) : ClassVisitor(Opcodes.ASM5, classVisitor) {

     private var className:String? = null
     private var superName:String? = null

    override fun visit(version: Int, access: Int, name: String?, signature: String?, superName: String?, interfaces: Array<out String>?) {
        super.visit(version, access, name, signature, superName, interfaces)
        this.className = name
        this.superName = superName
    }


    override fun visitMethod(access: Int, name: String, descriptor: String?, signature: String?, exceptions: Array<out String>?): MethodVisitor {
        val methodVisitor = cv.visitMethod(access,name,descriptor,signature,exceptions)
        //找到 androidX 包下的 Activity 类
        if (superName == "androidx/appcompat/app/AppCompatActivity"){
            //对 onCreate 方法处理 加入日志打印
            if (name.startsWith("onCreate")){
                println("do ASM ClassVisitor visitMethod onCreate")
                return ASMLifecycleMethodVisitor(methodVisitor, className!!, name)
            }
            if (name.startsWith("onStart")){
                println("do ASM ClassVisitor visitMethod onStart")
                return ASMLifecycleMethodVisitor(methodVisitor, className!!, name)
            }
            if (name.startsWith("onResume")){
                println("do ASM ClassVisitor visitMethod onResume")
                return ASMLifecycleMethodVisitor(methodVisitor, className!!, name)
            }
            if (name.startsWith("onRestart")){
                println("do ASM ClassVisitor visitMethod onRestart")
                return ASMLifecycleMethodVisitor(methodVisitor, className!!, name)
            }
            if (name.startsWith("onPause")){
                println("do ASM ClassVisitor visitMethod onPause")
                return ASMLifecycleMethodVisitor(methodVisitor, className!!, name)
            }
            if (name.startsWith("onStop")){
                println("do ASM ClassVisitor visitMethod onStop")
                return ASMLifecycleMethodVisitor(methodVisitor, className!!, name)
            }
            if (name.startsWith("onDestroy")){
                println("do ASM ClassVisitor visitMethod onDestroy")
                return ASMLifecycleMethodVisitor(methodVisitor, className!!, name)
            }
        }
        return methodVisitor
    }

    override fun visitEnd() {
        super.visitEnd()
    }
}
复制代码
  • 使用 MethodVisitor 执行字节码插桩
/**
 * @Description: 方法 Method Visitor 为每一个方法加入日志打印
 * @author maoqitian
 * @date 2020/11/13 0013 11:47
 */
class ASMLifecycleMethodVisitor(private val methodVisitor:MethodVisitor, private val className:String,private val methodName:String) : MethodVisitor(Opcodes.ASM5, methodVisitor) {


    //在方法执行前插入日志字节码
    override fun visitCode() {
        super.visitCode()
        println("do ASMLifecycleMethodVisitor visitCode method......")

        methodVisitor.visitLdcInsn("毛麒添")

        methodVisitor.visitLdcInsn("$className -> $methodName")
        //字节码 插入方法 日志
        methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false)
        methodVisitor.visitInsn(Opcodes.POP)
    }

    override fun visitEnd() {
        super.visitEnd()
    }
}
复制代码
  • 在Demo 项目中写了两个Activity,两个Activity都实现了一些生命周期方法,具体代码就不贴了,可自行查看demo源码编译运行项目

项目编译自动插桩完成

  • 插件效果展现,从 MainActivity 跳转 SecondActivity再返回以下,能够看到只有Activity实现了生命周期方法就会自动插入日志打印代码

自动插桩效果

  • 最后以一张图来讲明自定义插件介入修改字节码的过程

自定义插件介入插桩示意图

自定义插件调试

  • 插件写好了不免有 bug,这时就须要用到插件调试来解决问题。
  • 首先和平时调试同样,在插件代码须要打断点的地方点上断点

插件代码打断点

  • gradle 命令 daemon 进程执行编译等待 debug
gradlew assembleDebug -Dorg.gradle.daemon=false -Dorg.gradle.debug=true
复制代码

gradle命令daemon进程执行编译等待 debug

  • 添加 remote 编译配置,保持默认配置就行

remote编译配置

  • 点击 debug 进入断点调试成功

点击 debug 进入断点调试

控制台等待编译等待调试下一步

gradle系列文章

参考

相关文章
相关标签/搜索