Gradle 学习之 Task

5,015

本文内容大部分来自 Gradle 官方文档,英文 OK 的同学可以直接看官方文档。 本文示例代码已放在 zjxstar 的 GitHub

前言

上一篇文章中,我们学习了 Gradle 中 Project 的相关知识,也提到了简单 Task 的定义。一个 Project 里包含了多个 Task(任务),Gradle 的一系列操作都归功于 Task。本文将深入学习 Task 的相关知识,掌握如何创建、访问和配置一个 Task。

什么是 Task

Task(任务)表示构建过程中的单个原子工作,例如编译类或生成 javadoc。每个任务都属于某个 Project,在 Project 中可以通过任务名或者 TaskContainer 来访问任务。每个任务都有一个完全限定的路径,该路径在所有项目的所有任务中都是唯一的。路径是所在项目路径和任务名称的串联,使用 : 字符分隔。

当 Gradle 执行一个任务时,它可以在控制台 UI 和 Tooling API 中使用不同的结果标记任务。这些标签基于任务是否具有要执行的操作,是否应该执行这些操作,是否执行了这些操作以及这些操作是否进行了任何更改。

主要有以下 5 种标记:(在运行 Task 时增加 -i 或者 --info 即可查看)

  • 无标签或者 EXECUTED:任务执行了它的动作。
    1. 任务有动作,Gradle 已经确定它们应作为构建的一部分执行。
    2. 任务没有动作,但有一些依赖项,并且有依赖项得到了执行。
  • UP-TO-DATE:任务的输出没有改变。
    1. 任务有输出和输入,但没有改变。
    2. 任务有动作,但任务告诉 Gradle 它没有改变它的输出。
    3. 任务没有动作,有依赖,但所有的依赖项都是 up-to-date 的、跳过的或者来自缓存。
    4. 任务没有动作,也没有依赖项。
  • FROM-CACHE:任务的输出可以从之前的执行中找到。任务具有从构建缓存恢复的输出。
  • SKIPPED:任务没有执行它的动作。
    1. 任务已经明确从命令行中排除。
    2. 任务有一个返回 false 的 onlyIf 断言。
  • NO-SOURCE:任务不需要执行它的动作。任务有输入和输出,但是没有来源。

创建 Task

在 Gradle 中,我们有多种方式来创建任务。主要用到 Project 提供的 task 方法以及 TaskContainer ( tasks ) 提供的 create 方法。

方式一:使用字符串作为任务名创建 Task,示例:gradlew -q helloA

task('helloA') {
    doLast {
        println("helloA task(name)")
    }
}

方式二:使用 tasks 的 create 方法,示例:gradlew -q helloB

tasks.create('helloB') {
    doFirst {
        println("helloB tasks.create(name)")
    }
}

方式三:使用 DSL 的特殊语法,示例:gradlew -q helloC

task helloC {
    doLast {
        println("helloC task name")
    }
}

// 执行 gradlew -q helloD 命令运行
task(helloD) {
    doLast {
        println("helloD task(name)")
    }
}

在创建 Task 时,可以传入一个 Map 来简单配置任务,常用配置项有:

配置项 描述 默认值
type 基于一个存在的 Task 来创建,类似于继承 DefaultTask
overwrite 是否替换已经存在的 Task,和 type 配合使用 false
dependsOn 用于配置任务依赖 []
action 添加到任务中的一个 Action 或者一个闭包 null
description 用于配置任务的描述 null
group 用于配置任务的分组 null

使用示例:

// 使用 Map 增加配置项
task copy(type: Copy)

// 覆盖了原 copy 任务
task copy(overwrite: true) {
    doLast {
        println('I am the new one.')
    }
}

// 使用 gradlew tasks 命令查看 helloE 的配置
task helloE {
    description 'i am helloE'
    group BasePlugin.BUILD_GROUP
    doLast {
        println('this is helloE')
    }
}

以上是创建一个 Task 的基本方法。但在 build.gradle 脚本中,我们可以利用 Groovy 语言的强大特性来动态创建多个任务。例如:gradlew -q task1

// 同时创建4个任务:task0、task1、task2、task3
4.times { counter ->
    task "task$counter" {
        doLast {
            println "I'm task number $counter"
        }
    }
}

访问 Task

当创建完 Task 之后,我们可以访问它们以进行配置或者依赖。那么怎么访问一个已经定义好的 Task 呢?主要有三种方法。

方式一:使用 Groovy 中 DSL 特殊语法访问。

// 承接上文示例
// 以helloE任务为例
println '任务helloE的name: ' + helloE.name
println '任务helloE的description: ' + project.helloE.description

方式二:使用 tasks 访问任务集。

// 利用tasks
println tasks.named('helloD').get().name
println tasks.copy.doLast {
    println 'configure by tasks.copy.doLast'
}
println tasks['helloC'].name
println tasks.getByName('helloB').name

方式三:通过路径访问任务

println tasks.getByPath('helloE').path // 找不到抛异常UnknownTaskException
println tasks.getByPath(':app:helloE').path
def ehelloE = tasks.findByPath("EhelloE") // 找不到返回null;
println ehelloE == null

通过路径查找任务有两种方法,一种是 get,另一种是 find 。它们的区别在于 get 方法如果找不到指定任务就会抛出 UnknownTaskException 异常,而 find 方法则会返回 null 。

既然能够访问到 Task,那就可以对 Task 进行一些操作了,而这些操作又涉及到 Task 的属性和方法,这里简单介绍下。

Task 的属性范围有四个。你可以通过属性名或者 Task.property( java.lang.String ) 方法访问到指定属性。也可以通过 Task.setProperty( java.lang.String, java.lang.Object ) 方法修改属性值。四个范围如下:

  • Task 对象本身属性。这包括 Task 实现类中声明的任何带有 getters 和 setters 方法的属性。根据相应的 getter 和 setter 方法的存在,此范围的属性是可读写的。
  • 插件添加到任务的扩展。每个扩展名都可以作为只读属性使用,其名称与扩展名相同。
  • 通过插件添加到任务的约定属性。插件通过 Convention 对象向任务添加属性和方法。此范围的属性可读写性取决于约定对象。
  • 额外属性。每个任务对象都维护了一个附加属性的映射,是名称 -> 值对,可用于动态地向任务对象添加属性。此范围的属性是可读写的。

常用属性有:

属性名 描述
actions 该任务将要执行的一系列动作
dependsOn 返回该任务依赖的任务
description 任务的描述
enabled 该任务是否开启
finalizedBy 返回完成此任务之后的任务
group 任务的分组
mustRunAfter 返回该任务必须在哪个任务之后运行的任务
name 任务的名字
path 任务的路径
project 任务所属的 Project

我们举个额外属性的例子:

// 定义 Task 的额外属性
task myTask {
    ext.myProperty = "myValue"
}
// 访问该属性
task printTaskProperties {
    doLast {
        println myTask.myProperty
    }
}

其他的属性会在下面的小节中详细介绍。

至于 Task 的方法,这里只简单列举出来:

方法名(不列出参数) 描述
dependsOn 给任务设置依赖任务
doFirst 给 Task 添加一个任务动作开始执行之前的动作
doLast 给 Task 添加一个任务动作执行结束之后的动作
finalizedBy 给任务添加终结任务,即该任务结束后执行的任务
hasProperty 判断该任务是否有指定属性
mustRunAfter 声明该任务必须在某些任务之后执行
onlyIf 给任务添加断言,只有满足条件才可以执行任务
property 返回指定属性的值
setProperty 修改指定属性的值

前面的例子中经常看见 doFirst 和 doLast,下文中会对它们做详细的介绍。

DefaultTask

Gradle 其实还给我们提供一个 DefaultTask 基类,通过继承它可以用来自定义任务,多用在自定义 Gradle 插件中。这里简单写个示例,来说明 DefaultTask 的用法。

// app的build.gradle
class CustomTask extends DefaultTask {
    final String message
    final int number

    def content // 配置参数

    // 添加构造参数
    @Inject
    CustomTask(String message, int number) {
        this.message = message
        this.number = number
    }

    // 添加要执行动作
    @TaskAction
    def greet() {
        println content
        println "message is $message , number is $number !"
    }
}

// 使用tasks创建
// 需要传递两个参数,不能为null
tasks.create('myCustomTask1', CustomTask, 'hahaha', 110)
myCustomTask1.content = 'i love you'
myCustomTask1.doFirst {
    println 'my custom task do first'
}
myCustomTask1.doLast {
    println 'my custom task do last'
}

// 使用task创建,构造参数使用constructorArgs传递,参数不能为null
task myCustomTask2(type: CustomTask, constructorArgs: ['xixi', 120])
myCustomTask2.content = 'i hate you'

示例中的 CustomTask 类是在 build.gradle 文件中定义的,直接继承 DefaultTask 即可。如果希望该 Task 有可执行的动作,就需要在动作方法上添加 @TaskAction 的注解,这样 Gradle 就会将该动作添加到 Task 的动作列表中。我们可以在类中为任务配置属性,如示例中的 content 、message 、number 。其中,message 和 number 通过 @javax.inject.Inject 注解设置成了构造参数,在创建该自定义任务时需要传递这两个参数。

Task 的执行分析

讲到这里,我们已经了解了 Task 的创建与使用,那么现在有必要对 Task 的执行做一个大概的分析,这对我们深入理解 Task 有很大帮助。

当我们执行一个 Task 的时候,其实是执行其拥有的 actions 列表,这个列表保存在 Task 对象实例的 actions 成员变量中,其类型是一个 List:

private List<ContextAwareTaskAction> actions;

那么怎么将执行动作加入到该列表中呢?在前文的示例代码中,大家应该注意到创建 Task 的时候用到了 doFirst 和 doLast 两个方法。没错,这两个方法就可以将待执行的动作添加到 actions 列表中。

我们粗略看下 doFirst 和 doLast 的源码实现:(这两个方法在类 org.gradle.api.internal.AbstractTask 中)

@Override
public Task doFirst(final String actionName, final Action<? super Task> action) {
    ...
    taskMutator.mutate("Task.doFirst(Action)", new Runnable() {
        public void run() {
            // 每次都添加到列表头
            getTaskActions().add(0, wrap(action, actionName));
        }
    });
    return this;
}

@Override
public Task doLast(final String actionName, final Action<? super Task> action) {
    ...
    taskMutator.mutate("Task.doLast(Action)", new Runnable() {
        public void run() {
            // 每次都添加到表尾
            getTaskActions().add(wrap(action, actionName));
        }
    });
    return this;
}

可以看到,doFirst 会把动作添加到表头,而 doLast 则会把动作添加到表尾。

那怎么把动作添加到列表中间呢?是不是直接在 Task 里写动作?我们可以试一下:

// 通过 gradlew -q greetC 执行
task greetC { // 可以显示声明 doFirst doLast
    // 在配置阶段执行
    println 'i am greet C, in configure'

    // 在执行阶段执行
    doFirst {
        println 'i am greet C, in doFirst'
    }

    // 在执行阶段执行
    doLast {
        println 'i am greet C, in doLast'
    }
}

执行该任务后,你会发现 " i am greet C, in configure " 这句话没有在 doFirst 和 doLast 中间打印,而是打印在任务的配置阶段。说明直接在 Task 里写的动作并不会添加到 Task 的动作列表中,只会当做 Task 的配置信息执行。那有没有其他办法呢?

答案是肯定的,这就得利用到上小节中提到的 DefaultTask 类了。我们用 @TaskAction 注解标记的动作就会被添加到 Task 的动作列表中间。我们直接执行上节中的 myCustomTask1 任务 ( gradlew -q myCustomTask1 ) 。结果如下:

my custom task do first // doFirst
i love you // @TaskAction
message is hahaha , number is 110 ! // @TaskAction
my custom task do last // doLast

这样,Task 的执行顺序基本就清晰了。

这里需要提一个注意点,我们在创建任务的时候,如果只想给任务配置一个 doFirst 的动作,可以使用左移符号 << 来表示。

task greetB << { // << 等价于 doFirst
    println 'i am greet B, in doFirst'

    // 不能继续配置doLast
//    doLast {
//        println 'i am greet B, in doLast'
//    }
}
// 只能单独配置
greetB.doLast {
    println 'i am greet B, in doLast'
}

左移符号就等价于 doFirst ,而且,此时不能再给该任务配置 doLast 动作了,只能单独进行配置。

Task 依赖和顺序

我们知道,一个 Project 拥有多个 Task,这些 Task 之间的关系由 DAG 图维护。而 DAG 图是在构建的配置过程中生成的,我们可以通过 gradle.taskGraph 来监听这个过程。

举例:这个方法常用来给某些变量赋不同的值。( gradlew -q greetD )

def versionD = '0.0'
task greetD {
    doLast {
        println "i am greet D, versionD is $versionD"
    }
}
// task的DAG图是在配置阶段生成的
// 任务准备好了
gradle.taskGraph.whenReady {taskGraph ->
    if (taskGraph.hasTask('greetC')) {
        versionD = '1.0'
    } else {
        versionD = '1.0-alpha'
    }
}
// 任务执行前
gradle.taskGraph.beforeTask { Task task ->
    println "executing $task ..."
}
// 任务执行后
gradle.taskGraph.afterTask { Task task, TaskState state ->
    if (state.failure) {
        println "FAILED"
    } else {
        println "$task done"
    }
}

我们可以通过监听 DAG 图的回调来对特定的 Task 进行定制化处理。

既然任务是通过 DAG 图维护的,那任务之间肯定存在依赖和先后执行顺序,我们在定义任务的时候是否也可以给任务添加依赖或者执行顺序呢?这就得利用到任务的 dependsOn 、mustRunAfter 等方法了。

  • dependsOn :给某个任务设置依赖任务。

    task dependsA { // 定义一个基础Task
        doLast {
            println 'i am depends A task'
        }
    }
    
    // 当执行B时,会先执行它的依赖任务A
    task dependsB {
        dependsOn dependsA // 通过方法设置
        doLast {
            println 'i am depends B task'
        }
    }
    
    // 通过Map参数依赖任务A
    task dependsC(dependsOn: dependsA) {
        doLast {
            println 'i am depends C task'
        }
    }
    
    // 任务D懒依赖任务E
    // 任务E后定义
    task dependsD {
        dependsOn 'dependsE'
        doLast {
            println 'i am depends D task'
        }
    }
    
    task dependsE {
        doLast {
            println 'i am depends E task'
        }
    }
    
    task dependsF {
        doLast {
            println 'i am depends F task'
        }
    }
    // 通过dependsOn方法同时依赖两个任务E和A
    dependsF.dependsOn dependsE, dependsA
    

    从示例中可以看到,通过 dependsOn 设置任务的依赖关系后,当执行任务时,其依赖的任务会先完成执行。而且,可以给某个任务同时设置多个依赖任务;也可以进行懒依赖,即依赖那些还没有定义的任务。

  • finalizedBy :给某个任务设置终结任务。

    task taskX { // 定义任务X
        doLast {
            println 'i am task X'
        }
    }
    
    task taskY { // 定义任务Y
        doLast {
            println 'i am task Y'
        }
    }
    
    task taskZ { // 定义任务Z
        doLast {
            println 'i am task Z'
        }
    }
    // 任务X执行后,立刻执行任务Y和任务Z
    taskX.finalizedBy taskY, taskZ
    
    task taskM { // 定义任务M
        doLast {
            println 'i am task M'
        }
    }
    
    task taskN {
        finalizedBy taskM // 将任务M设置成任务N的终结任务
        doLast {
            println 'i am task N'
        }
    }
    

    对任务进行 finalizedBy 配置和 dependsOn 很类似,其作用和 dependsOn 恰好相反。在某任务执行完后,会执行其设置的终结任务。

  • mustRunAfter :如果 taskB.mustRunAfter(taskA) 则表示 taskB 必须在 taskA 执行之后再执行,这个规则比较严格。

    task taskA {
        doLast {
            println 'i am task A'
        }
    }
    
    task taskB {
        doLast {
            println 'i am task B'
        }
    }
    // 任务A必须在任务B之后执行
    taskA.mustRunAfter taskB
    

    运行命令 gradlew taskA taskB ,你会发现 taskB 会先执行。

  • shouldRunAfter :如果 taskB.shouldRunAfter(taskA) 则表示 taskB 应该在 taskA 之后执行,但这不是必须的,任务可能不会按预设的顺序执行。

    task shouldTaskC << {
        println 'i am task C'
    }
    
    task shouldTaskD << {
        println 'i am task D'
    }
    
    shouldTaskC.shouldRunAfter shouldTaskD
    

    运行命令 gradlew shouldTaskC shouldTaskD 查看执行结果。

    这里举个 shouldRunAfter 失效的例子:

    task shouldTaskX {
        doLast {
            println 'taskX'
        }
    }
    task shouldTaskY {
        doLast {
            println 'taskY'
        }
    }
    task shouldTaskZ {
        doLast {
            println 'taskZ'
        }
    }
    shouldTaskX.dependsOn shouldTaskY
    shouldTaskY.dependsOn shouldTaskZ
    shouldTaskZ.shouldRunAfter shouldTaskX // 这里其实是失效的
    

    运行命令 gradlew -q shouldTaskX 会发现任务 Z 会先于任务 X 执行。

这里再提一个 tasks 的 whenTaskAdded 方法。如果在构建过程中有任务添加到 project ,则会触发此回调。我们可以监听这个回调来配置一些任务依赖或者修改某些变量,示例如下 ( gradlew HelloSecond )。

// 定义任务greetE
tasks.create('greetE') {
    doLast {
        println 'i am greetE'
    }
}
// 构建过程中添加任务时会触发此回调,常用来配置一些任务依赖或者赋值
// 经测试,该回调只针对插件中的任务有效
project.tasks.whenTaskAdded { task ->
    println "task ${task.name} add"
    if (task.name == 'HelloSecond') { // 执行HelloSecond任务时,会先执行greetE
        task.dependsOn 'greetE'
    }
}

// 定义一个自定义插件
class SecondPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
        println 'Hello Second Gradle Plugin'
        // 添加一个task到project中
        project.task('HelloSecond', {
            println '===== SecondPlugin HelloSecond Task ====='
            logger.quiet('hello')
        })
    }
}

apply plugin: SecondPlugin

该回调只针对插件(插件的相关知识会在后续文章中给大家介绍)中定义的任务,示例中为插件中的 HelloSecond 任务添加了一个 greetE 任务依赖。这样,执行 HelloSecond 时会先执行 greetE 。

跳过 Task

可能你有这样的需求:某些任务禁止执行或者满足某个条件才能执行。Gradle 提供了多种方式来跳过任务。

方式一:每个任务都有个 enabled 属性,可以启用和禁用任务,默认是 true,表示启用。如果设置为 false ,则会禁止该任务执行,输出会提示该任务被跳过,即被标记成 SKIPPED 。

// 使用gradlew disableMe运行
// 输出:Task :app:disableMe SKIPPED
task disableMe {
    doLast {
        println 'This should not be printed if the task is disabled.'
    }
}
disableMe.enabled = false // 禁止该任务执行

方式二:使用 onlyIf 判断方法。只有当 onlyIf 里返回 true 时该任务才可以执行。

// 使用gradlew sayBye -PskipSayBye运行
// 这里的-P是添加参数的意思
task sayBye {
    doLast {
        println 'i am sayBye task'
    }
}
// 只有当project中没有 skipSayBye 属性时,任务才可以执行
sayBye.onlyIf { !project.hasProperty('skipSayBye') }

方式三:使用 StopExecutionException 。如果任务抛出这个异常,Gradle 会跳过该任务的执行,转而去执行下一个任务。

// 使用gradlew nextTask运行
task byeTask {
    doLast {
        println 'We are doing the byeTask.'
    }
}
// 不会影响后续任务的执行
byeTask.doFirst {
    // Here you would put arbitrary conditions in real life.
    // But this is used in an integration test so we want defined behavior.
    if (true) { throw new StopExecutionException() }
}
task nextTask {
    dependsOn('byeTask') // 先执行byeTask,而byeTask会异常中断
    doLast { // 并不影响nextTask的执行
        println 'I am not affected'
    }
}

方式四:利用 Task 的 timeout 属性来限制任务的执行时间。一旦任务超时,它的执行就会被中断,任务将被标记失败。Gradle 中内置任务都能及时响应超时。

// 故意超时
task hangingTask() {
    doLast {
        Thread.sleep(100000)
    }
    timeout = Duration.ofMillis(500)
}

虽然有四种方法,但常用的还是方法一和方法二。

Task 规则

如果我们在要执行某个不存在的任务时,Gradle 会直接报异常提示找不到该任务。其实,我们可以通过添加规则的方式来做自定义处理。这需要利用到 TaskContainer 的 addRule 方法。

// 第一个参数是该规则的描述
// 第二个闭包参数是该规则要执行的动作
tasks.addRule("Pattern: ping<ID>") { String taskName ->
    if (taskName.startsWith("ping")) {
        task(taskName) {
            doLast {
                println "Pinging: " + (taskName - 'ping')
            }
        }
    }
}

例子中添加了一个规则:如果运行的任务是以 ping 开头的,则会创建该任务(该任务运行前并不存在),并赋予 doLast 操作。可以使用 gradlew pingServer1 执行。

我们不仅可以通过命令行来使用规则,也可以在基于规则的任务上创建 dependsOn 关系。

// gradlew groupPing
task groupPing {
    dependsOn pingServer1, pingServer2
}

生命周期任务

生命周期任务是不能自行完成的任务,它们通常没有任何执行动作。这些任务主要体现在:

  • 工作流程任务,如: check ;
  • 一个可构建的东西,如: debug32MainExecutable ;
  • 用于执行多个有相同逻辑任务的便利任务,如: compileAll ;

除非生命周期任务具有操作,否则其结果由其依赖性决定。 如果执行任何任务的依赖项,则将认为生命周期任务已执行。 如果所有任务的依赖项都是最新的,跳过的或来自缓存,则生命周期任务将被视为最新的。

总结

读完本文内容,相信你已经学会了如何创建和使用 Task 。Task 作为 Gradle 的主要执行骨架是非常重要的,我们可以通过 Task 的各种属性、方法来灵活地配置和调整任务的依赖、执行顺序以及运行规则。大家不妨多写一些示例,有助于理解 Gradle 的工作机制,也为后续的自定义插件奠定基础。

参考资料

  1. Gradle 官方文档
  2. 《Android Gradle权威指南》
  3. 《Gradle for Android 中文版》