Gradle-任务

任务结果标签

当 Gradle 执行一个任务时,它会在控制台和 Tooling API 根据任务结果给任务打标签。

这些标签是根据任务是否有操作,是否应该执行操作,是否执行了操作以及这些操作做了哪些改变 来标记的。

下面是 Gradle 的标签以及对应的条件

  • (无标签)或者 EXECUTED

    任务执行了它的操作。

    • 任务有操作并且 Gradle 已经决定作为构建的一部分来执行
    • 任务没有操作但有些依赖,并且执行了某些依赖项。参考下面的生命周期任务。
  • UP-TO-DATE

    任务输出没有变化

    • 任务有输入和输出,但没有改变。另行参考增量构建
    • 任务有操作,但是没有改变它的输出。
    • 任务没有操作但是有些依赖,但所有的依赖都是最新的,忽略的或者来自缓存。参考下面的生命周期任务
  • FROM-CACHE

    任务的输出能够在先前的任务执行中被找到

    • 任务输出能够从缓存中恢复。具体的可另行参考 构建缓存
  • SKIPPED

    任务没有执行它的操作

    • 任务已经明确的在命令行中被排除。
    • 任务的 onlyIf 返回 false
  • NO-SOURCE

    任务不需要执行它的操作

    • 任务有输出和输入,但是没有来源。例如输入是空的。

在Android studio 的 build console 就可以看到这个标签

Android studio Build console

Tooling API 是指 Gradle 提供的编程 API,通常给 IDE,CI 服务器使用的

创建任务

最简单方便的定义方式:定义一个 hello 任务

这是使用 DSL 的语法

1
2
3
4
5
task hello {
doLast {
println 'Hello world!'
}
}

这里的 task 看着像一个关键字,实际上是一个方法,这个方法的原型是 TaskContainer.create()

任务的创建就是使用这个方法给 Project 添加一个 Task 类型的属性,名字就是我们的任务名字;所以才能使用任务名字引用一些API,例如为任务添加额外的属性。

也可以使用一个字符串当做任务名称,指定一个类型

1
2
3
4
5
6
7
8
9
10
task('hello') {
doLast {
println "hello"
}
}

task('copy', type: Copy) {
from(file('srcDir'))
into(buildDir)
}

或者是直接使用 tasks.create 方法; tasks 是 Project的一属性 类型是 TaskContainer

1
2
3
4
5
6
7
8
9
10
tasks.create('hello') {
doLast {
println "hello"
}
}

tasks.create('copy', Copy) {
from(file('srcDir'))
into(buildDir)
}

任务配置

在创建任务的的时候,可以传入参数对任务进行配置,比如 任务分组,任务描述等等

1
2
3
4
5
task(hello,group:'Hello',description:'这是一个 Hello。'){
doLast{
println "name:$name; group:$group; description:$description;"
}
}

输出如下

1
2
3
4
> Task :hello
name:hello; group:Hello; description:这是一个 Hello。;

BUILD SUCCESSFUL in 694ms

也可以在闭包中对任务进行配置

1
2
3
4
5
6
7
8
9
10
11
task taskX{
group 'Hello'
description '在闭包里配置'
dependsOn hello
}

tasks.create('taskY',Copy){
description '使用 tasks.create()'
dependsOn hello
group 'Hello'
}

使用 gradle help --task 任务名 查看任务详情

任务详情

可以配置的参数如下

配置项 描述 默认值
type 基于一个存在的 Task 来创建,和我们的类继承差不多 DefaultTask
dependsOn 用于配置任务的依赖 []
action 添加到任务的一个 Action 或者一个闭包 null
description 任务描述 null
group 任务分组 null

任务访问

通常是在配置任务或者是使用任务依赖的时候访问任务。

下面是几种获取任务引用的方式

使用 DSL 语法的方式

1
2
3
4
5
6
7
8
9
10
task hello
task copy(type: Copy)

// Access tasks using Groovy dynamic properties on Project

println hello.name
println project.hello.name

println copy.destinationDir
println project.copy.destinationDir

使用 tasks 集合访问

1
2
3
4
5
6
7
8
task hello
task copy(type: Copy)

println tasks.hello.name
println tasks.named('hello').get().name

println tasks.copy.destinationDir
println tasks.named('copy').get().destinationDir

使用 tasks.getByPath() 方法

可以在构建文件的任何地方使用这个方法,它接受任务名字,任务相对路径或者绝对路径。

1
2
3
4
5
6
7
8
9
10
project(':projectA') {
task hello
}

task hello

println tasks.getByPath('hello').path
println tasks.getByPath(':hello').path
println tasks.getByPath('projectA:hello').path
println tasks.getByPath(':projectA:hello').path

输出如下

gradle -q hello

1
2
3
4
:hello
:hello
:projectA:hello
:projectA:hello

当我们拿到这个任务的引用的时候,就可以按照我们的任务逻辑去操作它,比如配置任务依赖,配置任务的一些属性,调用方法等。

添加操作

可以使用 Task.doLast 和 Task.doFirst 为任务添加操作

其中 doFirst 是在任务之前执行,doLast 在任务之后执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

task taskZ

taskZ.configure {
group 'Custom'
description '添加操作的实验'
}

taskZ.doFirst {
println 'add doFirst. first'
}

taskZ.doLast {
println 'add doLast. first'
}
taskZ.doLast {
println 'add doLast. second'
}
taskZ.doFirst {
println 'add doFirst. second'
}

gradle taskZ

1
2
3
4
5
6
7
> Task :taskZ
add doFirst. second
add doFirst. first
add doLast. first
add doLast. second

BUILD SUCCESSFUL in 608ms

从输出可以看到是先执行 doFirst 然后是 doLast 。

执行分析

任务的执行其实就是执行它的 actions 列表。

这个列表保存在 Task 对象实例中的 actions 成员变量中,其类型是一个 List

1
private List<InputChangesAwareTaskAction> actions;

现在我们把 Task 之前执行、Task 本身执行以及 Task 之后执行分别称为 doFirst,doSelf,doLast。下面做个演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class CustomTask extends DefaultTask{

@TaskAction
def doSelf(){
println 'Task 自己本身在执行 in doSelf.'
}
}

task taskA(type:CustomTask)

taskA.doFirst {
println "do First. Task 之前执行"
}

taskA.doLast {
println "do Last. Task 之后执行"
}

例子中定义了一个 CustomTask 并声明了一个 doSelf 方法,使用 @TaskAction 标注,意思是这是任务本身要执行的方法。

执行 taskA 输出

1
2
3
4
5
6
> Task :taskA
do First. Task 之前执行
Task 自己本身在执行 in doSelf.
do Last. Task 之后执行

BUILD SUCCESSFUL in 726ms

前面说过任务执行就是执行它的 actions List。那么要达到这种 doFirst、doSelf、doLast 顺序的目的,就必须把 doFirst 的 actions 放在 actions List 的前面,把 doSelf 的 actions 放在 List 的中间,把 doLast 的 actions 放在 List 最后面,这样才能达到按约定顺序执行的目的。

当我们使用 task 方法创建 taskA 这个任务的时候,Gradle 会解析其带有 Task Action 标注的方法作为其 Task 执行的 Action,然后通过 Task 的 prependParallelSafeAction 方法把该 Action 添加到 actions List 里。

1
2
3
4
5
6
7
@Override
public void prependParallelSafeAction(final Action<? super Task> action) {
if (action == null) {
throw new InvalidUserDataException("Action must not be null!");
}
getTaskActions().add(0, wrap(action));
}

这个时候任务刚刚被创建,不会有 doFirst 的 Action, actions List 一般是空的。

只有在创建任务时,传入了配置参数中的 action 选项配置的时候才会有。(上面配置任务有提到)

这个时候 actions List 就有了任务本身的 Action了。

再来看 doFirst 和 doLast 两个方法的实现代码

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
27
28
29
30
31
32
33
34
35
36
37
38
39
@Override
public Task doFirst(final Action<? super Task> action) {
return doFirst("doFirst {} action", action);
}

@Override
public Task doFirst(final String actionName, final Action<? super Task> action) {
hasCustomActions = true;
if (action == null) {
throw new InvalidUserDataException("Action must not be null!");
}
taskMutator.mutate("Task.doFirst(Action)", new Runnable() {
@Override
public void run() {
getTaskActions().add(0, wrap(action, actionName));
}
});
return this;
}

@Override
public Task doLast(final Action<? super Task> action) {
return doLast("doLast {} action", action);
}

@Override
public Task doLast(final String actionName, final Action<? super Task> action) {
hasCustomActions = true;
if (action == null) {
throw new InvalidUserDataException("Action must not be null!");
}
taskMutator.mutate("Task.doLast(Action)", new Runnable() {
@Override
public void run() {
getTaskActions().add(wrap(action, actionName));
}
});
return this;
}

看最重要的 actions.add 这部分

doFirst 永远都是在 actions List 第一位添加,保证其添加的 Action 在现有的 actions List 的最前面。

doLast 永远都是在 actions List 末尾添加,保证其添加的 Action 在现有的 actions List 元素的最后面。

一个往最前面添加,一个往最后面添加,最后这个 actions List 就形成了 doFirst,doSelf,doLast三部分的 actions.

也就保证了 doFirst,doSelf,doLast三部分的 Action 顺序执行的目的。

这个任务执行分析在 《Android Gradle 权威指南》 中有很详细的解释。

任务排序

任务依赖也能够达到让任务排序的目的,但是还是有些区别的。

主要区别是排序不影响任务执行,只影响执行顺序。

有两种排序规则 “must run after” and “should run after”.

taskA.mustRunAfter(taskB) 表示 taskA 必须总是在 TaskB 之后运行。

taskA.shouldRunAfter(taskB) 表示 taskA 应该在 taskB 之后运行,并不一定必须运行,没有那么严格。

should run after 不会被执行通常是在两个场景下

  1. 陷入顺序循环
  2. 在执行任务依赖的时候如果满足了这个规则,将不会再次执行了。例如 再执行 taskB 的依赖时将 TaskA 给执行了,那么在 taskB 完成后将不会再执行 taskA。

must run after

1
2
3
4
5
6
7
8
9
10
11
task taskX {
doLast {
println 'taskX'
}
}
task taskY {
doLast {
println 'taskY'
}
}
taskY.mustRunAfter taskX
1
2
3
> gradle -q taskY taskX
taskX
taskY

should run after

1
2
3
4
5
6
7
8
9
10
11
task taskX {
doLast {
println 'taskX'
}
}
task taskY {
doLast {
println 'taskY'
}
}
taskY.shouldRunAfter taskX
1
2
3
> gradle -q taskY taskX
taskX
taskY

如果只执行一个任务的话顺序规则就没用了

gradle -q taskY

1
taskY

任务跳过

Gradle 提供了多种方式跳过任务,任务被跳过将不会执行。

使用断言 onlyIf

这个方法接收一个闭包参数,闭包返回 false 就不会执行,返回 true 将执行任务

这个方法是在执行任务前被调用的,不是在配置阶段。

1
2
3
4
5
6
7
task hello {
doLast {
println 'hello world'
}
}

hello.onlyIf { !project.hasProperty('skipHello') }

gradle hello -PskipHello

1
2
3
> Task :hello SKIPPED

BUILD SUCCESSFUL in 0s

使用 StopExecutionException

如果断言不能满足你的话,就可以使用这个 StopExecutionException ,使用这个异常可以执行时根据逻辑进行判断。

这个异常可以在一个操作中抛出,抛出后直接跳过这个任务进行下一个任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
task compile {
doLast {
println 'We are doing the compile.'
}
}

compile.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 myTask {
dependsOn('compile')
doLast {
println 'I am not affected'
}
}

gradle -q myTask

1
I am not affected

使用 enabled = false

每个任务都有一个 enabled 标志,默认是 true,如果设置为 false 这个任务将会被标记为 SKIPPED,直接跳过

1
2
3
4
5
6
task disableMe {
doLast {
println 'This should not be printed if the task is disabled.'
}
}
disableMe.enabled = false

执行 gradle disableMe

gradle disableMe

1
2
3
Task :disableMe SKIPPED

BUILD SUCCESSFUL in 0s

使用 超时时间

每个任务都有一个 timeout 属性用来限制执行时间。

当任务执行超时,任务执行线程就会被终止,任务将会被标记失败。

如果使用了 –continues 其他任务将会继续执行。

如果任务不能响应超时,任务将不会被终止。

Gradle 所有的内置任务都会响应超时。

1
2
3
4
5
6
task hangingTask() {
doLast {
Thread.sleep(100000)
}
timeout = Duration.ofMillis(500)
}

任务规则

正常情况下在使用 gradle 执行任务时,如果任务不存在就会抛出异常。

而任务规则就是在 Gradle 找不到任务时应用的规则,例如我们可以在找不到任务时打印一句话或者执行其他操作。

任务规则的添加和创建一样都是由 TaskContainer 完成,其实这个实在 NamedDomainObjectCollection 接口中的,不过 TaskContainer 继承了这个接口。

1
2
3
4
5
tasks.addRule("这就是一个任务规则描述"){ taskName ->
task (taskName){
println "$taskName 不存在"
}
}

这个 方法有多个重载,详情可查看 API。
API 传送门

生命周期任务

生命周期任务通常是没有操作的,通常是表达一个概念,例如下面几个:

  • 一个步骤,例如 check 检查,build 构建;
  • 一个可构建的东西,例如一个可执行文件
  • 一个组合了多个逻辑任务的空壳任务,例如 使用 compileAll 任务执行所有编译任务

Base Plugin 中定义了标准生命周期任务:

  • check
  • assemble
  • build

几乎所有的核心语言插件都应用了 Base Plugin, 例如 Java Plugin。

所以它的生命周期任务几乎所有语言插件都有。

除非生命周期任务有操作,否则它的结果标记应该是由它的依赖的执行结果决定的。

如果所有的依赖都被执行了,那么就应该标记 EXECUTED

如果所有的依赖都是最新的,跳过的或来自缓存,那么就应该被标记为 UP-TO-DATE

佛系编码 wechat
微信扫一扫,关注我的公众号
坚持原创技术分享,您的支持将鼓励我继续创作!