Yves

Hello Gradle Plugin

Gradle 是一个自动化构建工具,或者说构建框架.使用基于 Groovy 的 DSL 来进行声明配置,而不是传统的 XML。

Gradle 中有两个最基本的概念: project 和 task。一个 project 通常代表一个项目,在 Android 项目中,一个 module 就是一个 project,project 所在目录下有个 build.gradle 文件,负责当前 project 的构建配置。 一次构建可以有多个 project, 通过在项目根目录下的 settings.gradle 中声明有几个子 project。每一个 project 中又有很多个 task, 这些 task 中定义了构建过程中每个阶段应该执行的具体任务。

一定的 Groovy 基础

  • 基本集合类
  • 闭包和闭包委托
  • 灵活初始化和具名参数

Hello Task

官方文档中是这样说的:

Tasks are the cornerstone of getting things done in Gradle.

Task 是 Gradle 完成构建任务的基石。每个 Task 是构建过程中的原子操作,比如打一个 APK 包。在 Android 中执行 assemble 系列的 Task 就可以得到最终的 构建产物 —— APK 包,在执行 assemble 之前,会先执行其所以依赖的一系列的 Task。

举个栗子

首先在 build.gradle 文件里面写下以下代码:

1
2
3
4
5
task hello{
doLast{
println "Hello Task"
}
}

Gradle 是基于 Groovy 进行声明和配置的。上面的代码定义了一个名为 hello 的 Task。实际上调用的是一个方法,第一个参数是 task 的名字,第二个参数是一个闭包。

1
Task task(String name, Closure configureClosure);

我们在传入的闭包中执行了 doLast 方法,这个方法将在 Task 的最后一步执行,其参数又是一个闭包,这个比包里面就包含了我们自己定义的操作了。

执行命令 ./gradlew tasks --all |grep hello 就可以看到输出里面有 example:hello,example 是 project 所在的目录名。
执行这个 task 看看

1
2
:example:hello
Hello Task

这也体现了 Task 的原子性。这是一个简单的没有任何依赖关系的 Task

定义 Task 的时候也可以使用一个重载的操作符简化代码:

1
2
3
task hello << {
println "Hello Task"
}

效果与上面是一样的。但是官方并不建议使用这个操作符,因为这样会让代码的可读性降低,并在在 Gradle5.0 中将会弃用这个操作符

在定义的 Task 中添加描述。
在执行 ./gradlew tasks --all 的时候可以看见一些 Task 后面会跟着一些描述,用来说明这个 Task 是干嘛的,像这样:

1
example:assemble - Assembles all variants of all applications and secondary packages.

我们增加两行代码:

1
2
3
4
5
6
7
task hello{
group 'My Group'
description 'Prints Hello Task'
doLast{
println "Hello Task"
}
}

这时候执行 ./gradlew tasks,不需要再加 --all 参数,可以看到:

1
2
3
My Group tasks
--------------
hello - Prints Hello Task

把栗子举高点

Task 也是一个类,那么我们可以通过继承这个类实现一些扩展性更强的 Task
仍然是在 build.gradle 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Greeting extends DefaultTask {
String message
String recipient
@TaskAction
void sayGreeting() {
println "${message}, ${recipient}!"
}
}
task hello(type : Greeting){
group 'Welcome'
description 'Produces a world greeting'
message 'Hello'
recipient 'World'
}

第 5 行的注解标记该方法将是执行 Task 的入口。
第 11 行的意思很明显,Task 类型是上面定义的那个 Task。Android 项目顶层 build.gradle 中的 clean Task,就是通过这种方式定义的,只是 Delete Task 是 Gradle api 提供的。

1
2
3
task clean(type: Delete) {
delete rootProject.buildDir
}

Hello Plugin

在 Android 项目中,不同的 Variant(BuildTypes × Flavors) 都会有对应的一个生成 APK 的 assemble 系列 Task,执行 ./gradlew tasks |grep assemble 看看:

1
2
3
4
5
6
assemble - Assembles all variants of all applications and secondary packages.
assembleAndroidTest - Assembles all the Test applications.
assembleDebug - Assembles all Debug builds.
assembleF1 - Assembles all F1 builds.
assembleF2 - Assembles all F2 builds.
assembleRelease - Assembles all Release builds.

这就不可能全部在 build.gradle 里面定义了,是时候偷个懒了,重复的事情交给代码做。事实上,这些 Task 的生成,Android Gradle Plugin 已经帮我们做了,根据我们在 build.gradle 中传入的参数,自动生成不同 Varient 对应的 Task。

通过插件来做这些事情还有一个好处,就是更专注于构建什么而不是如何构建。具体构建的步骤,每个必要的 Task 之间的依赖关系封装在插件内部,只暴露出来参数的接口。除此之外,插件也比直接在 build.gradle 中实现更具有复用性。

一个简单的插件

  1. 新建一个 module,什么 module 都无所谓,然后修改 build.gradle

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    apply plugin : 'groovy'
    apply plugin: 'maven'
    dependencies{
    compile gradleApi()
    compile localGroovy()
    }
    repositories {
    jcenter()
    }
    group = 'cc.yvesluo.plugin' // 添加依赖时的 group
    version = '0.0.1' // 添加依赖时的 version
    // 发布插件到本地仓库
    uploadArchives {
    repositories {
    mavenDeployer {
    repository(url: uri('../repo'))
    }
    }
    }
  2. 创建文件 module/src/main/groovy/package/HelloPlugin.groovy

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    package com.example
    import org.gradle.api.Plugin
    import org.gradle.api.Project
    public class HelloPlugin implements Plugin<Project> {
    @Override
    void apply(Project project) {
    println "Hello Plugin"
    }
    }

    需要注意的是,创建的时候是 new > file,而不是 new > Java Class。 package 需要自己写,IDE 不会自动帮我们补全。HelloPlugin 类实现了 Plugin 接口,apply() 方法就是在 build.gradle 里面 执行 apply plugin: 'xxx' 的时候会执行的方法。

  3. 创建文件 module/src/main/resources/META-INF/gradle-plugins/hello.plugin.properties 文件

    1
    implementation-class=com.example.HelloPlugin

    文件的路径必须是这样的,但文件名可以是其它,但这个文件名是 apply 这个插件的时候的名字,比如现在如果要 apply 的话,build.gradle 中应该这样写:

    1
    apply plugin: 'hello.plugin.properties'
  4. 使用插件
    在 project(module) 的 build.gradle 中添加依赖后 apply

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    buildscript {
    repositories {
    maven {
    url uri('../repo')
    }
    jcenter()
    }
    dependencies {
    classpath 'cc.yvesluo.plugin:uploader:0.0.2' // group:moduleName:verison
    }
    }
    apply plugin: 'hello.plugin'

    sync 之后在 Gralde Console 窗口中可以找到输出了

    1
    Hello Plugin

    除了在子 project 的 build.gradle 中导入依赖,也可以在顶层的 build.gradle 中添加依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    // Top-level build file where you can add configuration options common to all sub-projects/modules.
    buildscript {
    repositories {
    jcenter()
    maven{
    url uri('./repo')
    }
    }
    dependencies {
    classpath 'com.android.tools.build:gradle:2.3.1'
    classpath 'cc.yvesluo.plugin:hello-plugin:0.0.1'
    // NOTE: Do not place your application dependencies here; they belong
    // in the individual module build.gradle files
    }
    }
    allprojects {
    repositories {
    jcenter()
    }
    }
    task clean(type: Delete) {
    delete rootProject.buildDir
    }

    那么两种方式有什么不一样呢?首先顶层的 build.gradle 是对所有的子 project 都生效的,其次如果插件还依赖了其它的库,那么在子 project 中是无法将插件所依赖的库也导进来的,需要另外再在顶层 build.gradle 中添加相关的依赖。

窥一斑而知全豹,虽然只是一个 Hello World 小 demo, 但万变不离其宗,无论是 Android 的 Gradle 插件,还是其他的 Gradle 插件,都是可以通过这样的方式实现的。

用插件创建 Task

前面完成了一个插件最基本的模型,那么接下来可以专注于插件的功能实现了。apply() 方法内的代码是在 build.gradle 脚本,执行到 apply plugin:'xxx' 的时候就会立即执行的,一些需要自动执行的操作可以放在这里,比如 Task 的创建。

1
2
3
4
5
6
7
8
@Override
void apply(Project project) {
println "Hello Plugin"
project.task("pluginTask0",{
println "pluginTask0"
})
}

这是通过插件创建 Task 的最简单的方式了。发布到仓库,apply 这个插件后 sync,就可以 ./gradlew pluginTask0 执行这个 Task

同样的,也可以定义一个 Task 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.example
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction
public class PluginTask extends DefaultTask {
PluginTask() {
}
@TaskAction
def run() {
println "This is PluginTask!"
}
}

然后在 plugin 类中创建

1
2
3
4
5
6
7
8
9
10
11
@Override
void apply(Project project) {
println "Hello Plugin"
project.task("pluginTask0",{
println "pluginTask0"
})
project.task("pluginTask",type:PluginTask)
}

Task 之间的依赖关系定义也很简单, Project#task() 方法返回一个 Task,调用 Task#dependsOn() 方法即可

1
project.task("pluginTask",type:PluginTask).dependsOn "pluginTask0"

DSL

定义 DSL,实际上就是自己规定从 build.gradle 脚本向插件传递参数时应该遵从什么样的格式。
首先定义参数 Extension 类

1
2
3
4
5
package com.example
public class HelloExtension{
def param
}

然后在 Plugin 类中通过相关方法创建实例,才能在 build.gradle 中使用 DSL。其实就是一句代码的事情

1
project.extensions.create("hello",HelloExtension);

在 Task 中使用之。也可以在 apply 中使用,但是必须要在脚本执行完之后才能取值,否则值将是空的

1
2
3
4
@TaskAction
def run() {
println "This is PluginTask! $project.hello.param"
}

最后是在 build.gradle 脚本中传参

1
2
3
4
5
6
7
apply plugin: 'hello.plugin'
hello{
param = "hello from script!"
// 等号可以省略,实际调用的是 setParam 方法
// param "hello from script!"
}