Gradle插件开发

gradle 生命周期

任务图(Task Graph)

首先要明白Gradle 核心是基于依赖的编程。具体来说是当你定义了任务和任务之间的依赖,gradle得保证这些任务按照他们的依赖顺序执行,所以gradle在执行任务之前,会构建一个任务图(Task Graph)。通过配置,Gradle 会跳过不属于当前构建的任务的配置。

在每个项目中,任务图最终会形成一个有向无环图(DAG)。

gradle-task-graph.png

构建阶段

Gradle 构建具有三个不同的阶段。Gradle 按顺序运行这些阶段:首先是初始化,然后是配置,最后是执行。

  • 初始化

    • 检测settting.gradle文件。
    • 评估settting.gradle文件以确定哪些项目和包含的构建参与构建。
    • 为每个项目创建一个Project实例。
  • 配置

    • 评估参与构建的每个项目的构建脚本。
    • 为请求的任务创建任务图。
  • 执行

    • 按照依赖关系的顺序安排和执行每个选定的任务。
// setting.gradle
rootProject.name = 'basic'
println 'This is executed during the initialization phase.'
// build.gradle
println 'This is executed during the configuration phase.'

tasks.register('configured') {
    println 'This is also executed during the configuration phase, because :configured is used in the build.'
}

tasks.register('test') {
    doLast {
        println 'This is executed during the execution phase.'
    }
}

tasks.register('testBoth') {
    doFirst {
      println 'This is executed first during the execution phase.'
    }
    doLast {
      println 'This is executed last during the execution phase.'
    }
    println 'This is executed during the configuration phase as well, because :testBoth is used in the build.'
}

具体来说,当以上gradle文件执行任务时,会先运行setting.gradle,比如Android项目中会有的include ':app',也会在这时注册app项目,创建其project实例。

然后在怕配置阶段会执行build.gradle,在这里会创建三个任务,但不会立即执行。

所以当执行以下命令时显示如下:

> gradle test testBoth
This is executed during the initialization phase.

> Configure project :
This is executed during the configuration phase.
This is executed during the configuration phase as well, because :testBoth is used in the build.

> Task :test
This is executed during the execution phase.

> Task :testBoth
This is executed first during the execution phase.
This is executed last during the execution phase.

BUILD SUCCESSFUL in 0s
2 actionable tasks: 2 executed

而gradle也为我们准备了一些钩子函数对其生命周期的各个阶段进行监听:

// 项目评估监听
gradle.beforeProject { project ->
    project.ext.set("hasTests", false)
}

gradle.afterProject { project ->
    // ...
}

// 任务监听
gradle.taskGraph.beforeTask { Task task ->
    println "executing $task ..."
}

gradle.taskGraph.afterTask { Task task, TaskState state ->
    if (state.failure) {
        println "FAILED"
    }
    else {
        println "done"
    }
}

gradle插件

gradle的核心是上述的任务自动化,但其所有的功能(如编译 Java 代码的能力)都是由插件提供的。gradle插件有点类似于代码中封装的方法,可以通过特定的配置,封装并且扩展项目的功能,减少多个项目维护相似的逻辑的开销。

插件类型分为两种:二进制插件和脚本插件。因为二进制插件可以以外部jar包的形式提供,所以通常运用的更普遍,这里以二进制插件为例。

声明插件

// build.gradle
plugins {
    id 'java' // 核心插件
    id 'com.jfrog.bintray' version '1.8.5' // 第三方社区插件
    // id «plugin id» version «plugin version» [apply «false»]
}

plugins块有如下限制:

  • 该plugins {}块还必须是构建脚本中的顶级语句。它不能嵌套在另一个构造中(例如 if 语句或 for 循环)。
  • 该plugins {}块目前只能在项目的构建脚本和 settings.gradle 文件中使用。它不能用于脚本插件或初始化脚本。

管理插件

可以在setting.gradle中配置pluginManagement {}块管理插件。它必须是文件中的第一个块

// setting.gradle
pluginManagement {
    plugins {
        id 'com.example.hello' version "${helloPluginVersion}"
    }
    repositories {
         maven {
            url './maven-repo'
        }
        gradlePluginPortal()
    }
}
// build.gradle
plugins {
    id 'com.example.hello'
}

约定插件

如果想定义一个自己的插件,可以新建一个module,或者在项目的buildSrc目录中新建build.gradle,并配置如下:

plugins {
    id 'java-gradle-plugin'
}

// 
gradlePlugin {
    plugins {
        myPlugins {
            id = 'my-plugin'
            implementationClass = 'my.MyPlugin'
        }
    }
}

这其实是 Java Gradle Plugin 提供的一个简化 API,其背后会自动帮我们创建一个 [插件ID].properties 配置文件,Gradle 就是通过这个文件类进行匹配的。如果你不使用 gradlePlugin API,直接手动创建 [插件ID].properties 文件,作用是完全一样的。

// my-plugin.properties
implementation-class=my.MyPlugin

然后在module中新建java/groovy/kotlin文件MyPlugin,继承自Plugin<Project>,并重写apply方法实现插件的逻辑。

class CyPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        // apply
    }
}

发布插件

通过 maven-publish 或 ivy-publish 发布:

// build.gradle
plugins {
    id 'java-gradle-plugin'
    id 'maven-publish'
    id 'ivy-publish'
}

group 'com.example'
version '1.0.0'

gradlePlugin {
    plugins {
        hello {
            id = 'com.example.hello'
            implementationClass = 'com.example.hello.HelloPlugin'
        }
        goodbye {
            id = 'com.example.goodbye'
            implementationClass = 'com.example.goodbye.GoodbyePlugin'
        }
    }
}

publishing {
    repositories {
        maven {
            url layout.buildDirectory.dir("maven-repo")
        }
        ivy {
            url layout.buildDirectory.dir("ivy-repo")
        }
    }
}

应用插件

在Gradle构建工具中,可以用buildScript{} 块来定义构建脚本自身的依赖关系,将已作为外部 jar 文件发布的二进制插件添加到项目中。

buildscript {
    repositories {
        gradlePluginPortal()
    }
    dependencies {
        classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.5'
    }
}

apply plugin: 'com.jfrog.bintray'

限制:buildscript {}块必须放在plugin {}块之前。

TASK Transform ASM

TASK

通用task

// android项目的clean task
task clean(type: Delete) {
    delete rootProject.buildDir
}

// 自定义task
tasks.register('hello')
// 自定义Copy类型的task
tasks.register('copy', Copy)

自定义task

首先继承defaultTask:

abstract class MyTask : DefaultTask() {
    @get:OutputFile
    abstract val outputFile: RegularFileProperty

    @get:Input
    abstract val inputCount: Property<Int>

    @TaskAction
    fun action() {
        // task执行的代码
        val outputFile = outputFile.get().asFile
        outputFile.delete()
        outputFile.parentFile.mkdirs()
        Files.write(outputFile.toPath(), ("Count is: " + inputCount.get()).toByteArray())
        println("MyTask Output file is: " + outputFile.toPath())
    }
}

注册:

project.tasks.register("myTask", MyTask::class.java) {
                it.inputCount.set(10)
                it.outputFile.set(File("build/myTask/output/file.txt"))
            }

上述任务执行的结果是生成app/build/myTask/output/file.txt,内容是

Count is: 10

Action

其实Task的本质是一组被顺序的Action对象构成。可以把Action理解为一段代码块。可通过在Task中添加doFirst{}和doLast{}来为Task执行Action的开始和结束添加Action。

task clean(type: Delete) {
   delete rootProject.buildDir
   doLast {
       println(prefix + "Android Studio auto add clean task do last")
   }
   doFirst {
       println(prefix + "Android Studio auto add clean task do first")
   }
}

task执行顺序

  1. B.dependsOn A:先执行完ATask,在执行BTask;
  2. B.mushRunAfter A:先执行完ATask,在执行BTask
  3. B.mushRunAfter A C.mushRunAfter A:按照ATask、BTask、CTask顺序执行
  4. B.shouldRunAfter A:先执行完ATask,在执行BTask

Transform

Transform API 是 AGP1.5 就引入的特性,主要用于在 Android 构建过程中,在 Class转Dex的过程中修改 Class 字节码。

Android 打包.png

自定义Transform流程:

public class DemoTransform extends Transform {
    Project project;

    public DemoTransform(Project project) {
        this.project = project;
    }

    // transform任务名字(用于尾部拼接)
    // 最终会生成 transformClassesWithDemoTransformForDebug 的Task
    @Override
    public String getName() {
        return "DemoTransform";
    }

    // Transform需要处理的类型
    @Override
    public Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS;
    }

    // transform作用域,要处理所有class字节码,Scope我们一般使用TransformManager.SCOPE_FULL_PROJECT
    @Override
    public Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT;
    }

    // 增量编译开关,true只有增量编译时才回生效
    @Override
    public boolean isIncremental() {
        return true;
    }

    @Override
    public void transform(TransformInvocation transformInvocation) throws IOException {
        System.out.println("============ DemoTransform 开始执行============");
        //消费型输入,可以从中获取jar包和class文件夹路径。需要输出给下一个任务
        final Collection<TransformInput> inputs = transformInvocation.getInputs();
        //引用型输入,无需输出。
        final Collection<TransformInput> referencedInputs = transformInvocation.getReferencedInputs();
        //OutputProvider管理输出路径
        final TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();

        for (TransformInput input : inputs) {
            // 处理jar文件
            for (JarInput jarInput : input.getJarInputs()) {
                System.out.println("jar= " + jarInput.getName());
                File dest = outputProvider.getContentLocation(
                        jarInput.getFile().getAbsolutePath(),
                        jarInput.getContentTypes(),
                        jarInput.getScopes(),
                        Format.JAR);
                // 将修改过的字节码copy到dest,就可以实现编译期间干预字节码的目的了
                FileUtils.copyFile(jarInput.getFile(), dest);
            }

            // 处理class
            for (DirectoryInput directoryInput : input.getDirectoryInputs()) {
                if (directoryInput.getFile().isDirectory()) {
                    for (File file : FileUtils.getAllFiles(directoryInput.getFile())) {
                        System.out.println("directoryInput--" + file.getName());
                    }
                }
                File dest = outputProvider.getContentLocation(
                        directoryInput.getName(),
                        directoryInput.getContentTypes(),
                        directoryInput.getScopes(),
                        Format.DIRECTORY);
                //建立文件夹
                FileUtils.mkdirs(dest);
                //将class文件及目录复制到dest路径
                FileUtils.copyDirectory(directoryInput.getFile(), dest);
            }

        }

    }
}

这里的关键是重写transform方法,里面可以从TransformInvocation中拿到所有打包到apk过程中需要的jar文件和class文件,所以这里我们就有机会对class文件进行干预,比如说分析或者改变class字节码。然后我们拿到dest目录(也就是我们拿到了这些文件,处理后需要将其移交给下一个transform),并将干预后的jar/class文件放到dest目录下。

需要注意的是这里我们就算什么文件都不想干预,也必须要将文件复制到dest目录下,否则打包会失败。

注册transform:

val appExtension = project.extensions.getByType(AppExtension::class.java)
appExtension.registerTransform(DemoTransform(project))

由此可见transform的效率并不高,因为每一个transform都会遍历整个打包过程中的jar/class文件,并且每一个transform我们都需要写上一堆重复的代码(获取jar - 遍历jar - 拿到class - 处理class - 打包jar - 复制/移动到dest目录)。

所以在AGP 7.0以后,transform被标记为弃用,并在AGP 8.0中被移除。取而代之的是 TransformActionAsmClassVisitorFactory

AGP 8.0 变化

以下是 AGP 8.0 的重要 API 更新

移除了 Transform API

从 AGP 8.0 开始,Transform API 将被移除。这意味着,软件包 com.android.build.api.transform 中的所有类都会被移除。

Transform API 即将被移除,以提高 build 的性能。使用 Transform API 的项目会强制 AGP 对 build 使用优化程度不够的流程,从而导致构建时间大幅增加。同时也很难使用 Transform API 以及将其与其他 Gradle 功能结合使用;这些替代 API 可让您更轻松地扩展 AGP,而不会引起性能问题或 build 正确性问题。

替代 API

Transform API 没有单一的替代 API,每个用例都会有新的针对性 API。所有替代 API 都位于 androidComponents {} 代码块中,在 AGP 7.2 中均有提供。

支持转换字节码

如需转换字节码,请使用 Instrumentation API。对于库,您只能为本地项目类注册插桩;对于应用和测试,您既可以选择仅为本地类注册插桩,也可以选择为所有类(包括本地和远程依赖项)注册插桩。为了使用此 API,每个类上的插桩都是独立运行的,并且对类路径中其他类的访问会受到限制(如需了解详情,请参见 createClassVisitor())。此限制提高了完整 build 和增量 build 的性能,并使得 API Surface 变得简单。每个库一旦准备就绪,即会进行并行插桩;而不是在所有编译完成后进行插桩。此外,如果是在单个类中做出更改,则意味着只有受影响的类必须在增量 build 中重新进行插桩。如需查看 Instrumentation API 使用方法的示例,请参阅使用 ASM 转换类 AGP 配方。

TransformAction

参考:Transform 被废弃,TransformAction 了解一下~

Transform API是由AGP提供的,而Transform Action则是由Gradle提供。不光是 AGP 需要 Transform,Java 也需要,所以由 Gradle 来提供统一的 Transform API。

关于 TransformAction 如何使用,Gradle 官方已经提供了很详细的文档–Transforming dependency artifacts on resolution,与 AGP 类似,也是需要先注册,只不过 AGP 是通过 Android Extension 来注册 Transform ,Gradle 是通过 DependencyHandler 来注册 TransformAction ,差异并不算很大。

// Plugin#apply()
val artifactType = Attribute.of("artifactType", String::class.java)
project.dependencies.registerTransform(MyTransformAction::class.java) {
    it.from.attribute(artifactType, "jar")
    it.to.attribute(artifactType, "my-custom-type")
}
abstract class MyTransformAction : TransformAction<TransformParameters.None> {
    @get:PathSensitive(PathSensitivity.NAME_ONLY)
    @get:InputArtifact
    abstract val inputArtifact: Provider<FileSystemLocation>

    override fun transform(outputs: TransformOutputs) {
        val file = inputArtifact.get().asFile;
        println("Processing $file. File exists = ${file.exists()}")
        if (file.exists()) {
            val outputFile = outputs.file("copy");
            Files.copy(file.toPath(), outputFile.toPath())
        } else {
            throw RuntimeException("File does not exist: " + file.canonicalPath);
        }
    }
}

具体的使用也可以看看AGP中自带的JetifyTransformAarTransform

AsmClassVisitorFactory

AGP 8.0文档中也提到了对字节码转换的支持,具体来说,就是AGP为我们又做了一层封装,提供了AsmClassVisitorFactory来方便我们使用Transform Action进行ASM操作。

ASM(全称:Java ASM)是一种 Java 字节码操纵框架,官网:https://asm.ow2.io/

如果是用transform api + asm 的方式实现字节码插桩,我们需要写很多模板式的代码,具体可以看看sensor埋点的实现。

但其实对于ASM而言,我们只需要通过提供不同的classVisitor实例,就可以实现我们特定的需求,至于怎么找到class,怎么通过classVisitor访问class就全是模板代码了,所以AsmClassVisitorFactory的发布就是为了解决这个痛点。

val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
androidComponents.onVariants {
        it.transformClassesWith(LogAsmTransform::class.java, InstrumentationScope.ALL) {
            // it -> InstrumentationParameters 携带参数
        }
    }
abstract class LogAsmTransform : AsmClassVisitorFactory<InstrumentationParameters.None> {

    override fun createClassVisitor(
        classContext: ClassContext,
        nextClassVisitor: ClassVisitor
    ): ClassVisitor {
        // 返回一个 ClassVisitor 对象,其内部实现了我们修改 class 文件的逻辑
        return object : ClassVisitor(Opcodes.ASM9, nextClassVisitor) {
            val className = classContext.currentClassData.className

            // 这里,由于只需要修改方法,故而只重载了 visitMethod 找个方法
            override fun visitMethod(
                access: Int,
                name: String?,
                descriptor: String?,
                signature: String?,
                exceptions: Array<out String>?
            ): MethodVisitor {
                val oldMethodVisitor =
                    super.visitMethod(access, name, descriptor, signature, exceptions)
                // 返回一个 MethodVisitor 对象,其内部实现了我们修改方法的逻辑
                return LogMethodVisitor(className, oldMethodVisitor, access, name, descriptor)
            }
        }
    }

    override fun isInstrumentable(classData: ClassData): Boolean {
        return true
    }
}

至于ASM的使用,又是一个大的范畴,故不在此篇做讲解。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,386评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,142评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,704评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,702评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,716评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,573评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,314评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,230评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,680评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,873评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,991评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,706评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,329评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,910评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,038评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,158评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,941评论 2 355

推荐阅读更多精彩内容