Gradle学习笔记-Task

Gradle的两个重要的概念:ProjectTask,一个Project由多个Task组成。

Task

Gradle脚本中的最小执行单元,也是Gradle中的一个原子操作。

Task Result(任务结果)

当Task执行时,最终控制台都会输出执行的结果,后面都会带有一个标签,这些标签表示了是否有Task需要执行,是否执行了Task等状态。

结果标签 结果描述 如何触发
没有标签
EXECUTED
任务执行完毕 任务有动作且被执行
UP-TO-DATE 任务输出没有改变 任务没有动作也没有依赖
任务有输入输出但是没有发生变化
任务没有动作但存在依赖,且依赖非执行完毕
NO-SOURCE 任务不需要执行 包含了输入输出,但是没有Sources???
FROM-CACHE 在缓存中找到了任务执行结果 构建缓存中已存在构建结果
SKIPPED 任务没有执行 指定跳过该任务
任务设置了onlyIf且返回false
任务被禁用enabled=false

Task Create(创建任务)

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
task createTask1 << {
println "doLast in createTask1"
}

task createTask2 doLast {
println "doLast in createTask2"
}

//三种方式皆可
project.task("createTask3").doLast {
println "doLast in createTask3"
}
project.task("createTask3") doLast {
println "doLast in createTask3"
}
project.task("createTask3") << {
println "doLast in createTask3"
}
//通过TaskContainer创建Task
project.tasks.create("createTask4").doLast {
println "doLast in createTask4"
}
project.tasks.create("createTask4") doLast {
println "doLast in createTask4"
}
project.tasks.create("createTask4") << {
println "doLast in createTask4"
}

其中<<等价于doLast,但是在Gradle 5.0之后该方法已被废弃。

上述只是基础的创建方法,创建时还包括了其他的参数。

参数名 含义 参数属性
name 任务名称 必须有值,不能为空
description 任务描述 可以为空
group 任务所属分组名 可以为空
type 任务的父类 默认为org.gradle.api.DefaultTask
dependsOn 任务依赖的其他Task 可以为空
overwrite 是否覆盖已存在的同名任务 false
constructorArgs 任务构造函数参数 可以为空(若依赖父类有构造参数,需要设置值)
action 任务的顺序执行序列 doLast(最后执行)doFirst(最先执行)

Task Action(执行序列)

一个Task由一系列Action组成的,通过设置action,实质上就是在创建Task时调用到的doFirstdoLast这两个方法。

1
2
3
4
5
6
7
8
9
task Task1 {
println "Task configure"
doFirst {
println "Task doFirst"
}
doLast {
println "Task doLast"
}
}

上述代码不同的执行方式结果不同

  • 执行整个gradle文件:

    1
    Task configure
  • 执行Task1./gradlew Task1

    1
    2
    3
    Task configure
    Task doFirst
    Task doLast

观察上述结果,得出以下结论

在创建Task时,除了doFirstdoLast之外的代码,都定义为Task的配置项,在脚本的配置阶段都会执行;而doFirstdoLast代码只会在Task真正执行时才会调用(gradle 指定运行该Task)。

Task DependsOn(执行依赖)

Gradle中的任务执行顺序是不确定的,需要通过task之间的依赖关系,保证被依赖的task优先执行,可通过dependsOn来确定依赖关系。

默认规则与Task名称相关,按照名称进行排序!

Task执行顺序

dependsOn 强依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
task first doLast {
println("first")
}

task second doLast {
println("second")
}
//second 依赖于 first
second.dependsOn(first)

//third 依赖于 first,second
task third(dependsOn:[first, second]) doLast {
println("third")
}

此时调用./gradlew third

1
2
3
4
5
6
7
8
9
输出结果
> Task :plugin:first
first

> Task :plugin:second
second

> Task :plugin:third
third

由于third依赖于first、second所以在执行third时,first、second也需要执行。

以上属于静态依赖

相对的还存在动态依赖

1
2
3
4
5
6
7
8
task forth {
dependsOn this.tasks.findAll { task ->
return task.name.startsWith("third")
}
doLast {
println "This is forth"
}
}

顺序依赖

此外可通过shouldRunAftermustRunAfter来控制任务之间的执行顺序

mustRunAfter

强制按照要求的顺序执行

shouldRunAfter

非强制的按照顺序执行,在以下两种情况会放弃此规则:

  1. TaskGraph为有向无环,可能导致环形放弃规则
  2. 并行执行任务并且所有任务的依赖项都已完成

Task Type(任务类型)

默认Type为DefaultTask,系统还提供了几种常用的类型以供使用,也可以通过自定义Type来实现功能。

Copy

将文件复制到目标目录,此任务在复制时也可以执行重命名和过滤文件操作。

1
2
3
4
5
6
task CopyFile(type:Copy){
//源文件目录
from '../app/src/main'
//目标目录
into './src/main/java'
}

frominto是最基础的配置,其他常用包括以下:

配置项 释义 示例
include 只包含配置的文件 include '**/*.java', '**/*.kt'
exclude 排除配置的文件 exclude '**/*.xml'
includeEmptyDirs 是否包括空文件夹 true文件夹下的所有文件夹也会被拷贝进来
false不会存在空文件夹
rename 对指定的文件进行重命名 rename 'activity_*.xml' 'rename'
with 执行一个闭包 def dataContent = copySpec {
from (‘../src/main’) {
include ‘*/.xml’
} }
with dataContent

Sync

与Copy任务类似,不同的是将源目录中的文件复制到目标目录中,但是会删除目标目录中非复制过来的文件。

1
2
3
4
5
6
7
task syncFile(type:Sync){
from '../app/src/main/java'
rename 'Main*', 'SSS'
into './src/main/java'

includeEmptyDirs = false
}

可通过设置preverse属性,控制哪些文件不会被覆盖

1
2
3
4
5
6
7
8
9
10
task syncFile(type:Sync){
from '../app/src/main/java'
rename 'Main*', 'SSS'
into './src/main/java'

includeEmptyDirs = false
preserve {
include '**/*.xml'
}
}

那么目标目录原有的xml不会被删除

其他类型

通过官网介绍来查询其他系统支持类型

自定义Type

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
//设置父类
class ParentTask extends DefaultTask{
String msg = "parent"
int age
int score

@Inject
ParentTask(int age,int score){
this.age = age
this.score = score
}

@TaskAction
void sayHello(){
println "hello $msg age is $age and score is $score"
}

}

//设置type即父类为 ParentTask 设置参数为 30,100
task Task1(type:ParentTask,constructorArgs:[30,100])

task Task2(type: ParentTask,constructorArgs: [10,70]){
msg="wxy"
}

输出结果:
> Task :plugin:Task1
hello parent age is 30 and score is 100

> Task :plugin:Task2
hello wxy age is 10 and score is 70

Task Group(任务分组)&Task Description(任务描述)

对任务进行分组整理,使结构清晰明了

对任务进行描述,说明任务的作用

1
2
3
4
5
6
task MyTask(description:"Task的介绍",group:"MyTaskGroup") doLast {
println "group $group "
}

> Task :plugin:MyTask
group is MyTaskGroup and description is Task的介绍

可以通过执行./gradlew -q tasks --all查看所有task信息

1
2
3
MyTaskGroup tasks
-----------------
plugin:MyTask - Task的介绍

Task Overwrite(任务重写)

对上面的任务进行覆盖,后续只会执行该任务

1
2
3
4
5
6
7
task MyTask(description:"Task的介绍",group:"MyTaskGroup") doLast {
println "group is $group and description is $description"
}

task MyTask(overwrite:true) doLast {
println "Cover Same Task"
}

后续只会输出Cover Same Task

Task Enable(任务启用)

通过设置enabled属性,用于启用和禁用任务,默认为true,表示启用。false则禁止该任务执行

1
2
3
task MyTask {
enabled false
}

运行会提示 Task :plugin:zipFile SKIPPED

TaskContainer(任务集合)

管理所有的Task实例,可通过Project.getTasks()或者tasks使用该实例

提供了以下常用的方法

方法 介绍
create(name:String) : Task 创建任务
create(name:String,configureClosure:Closure) : Task 创建任务
create(options: Map<String, ?>,configure: Closure): Task 创建任务
findByPath(path: String): Task 查找任务
getByName(name: String): Task 根据Task名字查找任务
withType(type: Class): TaskCollection 根据Type查找任务
register(String name):TaskProvider 按需加载任务
replace(String name):Task 替换当前同名任务
remove(Task task) 删除任务
whenTaskAdded(action:Closure) task添加进TaskContainer时监听
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
//创建Task
tasks.create("Task1"){}
tasks.create("Task2", Copy.class){
from '../app/src/main/java'
into './src/main/java'
}
tasks.create([name:"Task3",group:"customGroup",desription:"desc",dependsOn:["Task1"]]){

}

//查找Task
def task1 = tasks.findByName("Task1")
def task2 = tasks.withType(Copy.class)

//替换Task
tasks.replace("Task1"){


}

//监听Task添加
tasks.whenTaskAdded { task->
if(task.name == "Task1" ){
println "Task1 is added"
}else{
println "${task.name} is added"
}
}

Task增量构建

Task会缓存每次运行的结果,在下次运行时会检查输出结果是否进行改变,没有发生变化就会跳过当次运行 。为了提高Gradle的编译速度

在控制台会显示up-to-date表示跳过该次执行。

Gradle通过对比上一次构建之后,Task的inputs和outputs是否发生变化,来决定是否跳过执行。

Task Input/Output(任务输入/输出)

大多数情况下,Task需要接收一些Input(输入),并生成一些Output(输出)

Task Input/Output示例

上图是一个简单的示例:

通过Input——TargetJDKVersion , Sourcefiles经过JavaCompileTask处理后得到Output——Class files

可以将其理解为一个函数,input为入参,output为返回值。

还有两个关键点:

  • 隐式依赖:若Task的输入为另一Task的输出,则会被推断出依赖关系。
  • 配置阶段声明

在定义Task的输入输出时,要遵循一个原则:只有Task的属性会影响输出,就需要把该属性注册为输入。

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class VersionMsg {
String versionCode
String versionName
String versionInfo
}
ext{
destFile = file('releases.xml')
}


task writeTask() {
doFirst {
if (destFile != null && !destFile.exists()) {
destFile.createNewFile()
}
}

inputs.property('versionCode', "2")
inputs.property('versionName', "1.0.0")
inputs.property('versionInfo', "init")

outputs.file destFile
doLast {
def data = inputs.getProperties()
File file = outputs.getFiles().getSingleFile()

def versionMsg = new VersionMsg(data)
//将实体对象写入到xml文件中
def sw = new StringWriter()
def xmlBuilder = new MarkupBuilder(sw)
//没有内容
xmlBuilder.releases {
release {
versionCode(versionMsg.versionCode)
versionName(versionMsg.versionName)
versionInfo(versionMsg.versionInfo)
}
}
//直接写入
file.withWriter { writer -> writer.append(sw.toString())
}
}
}

task zreadTask() {
inputs.file this.destFile
doLast {
//读取输入文件的内容并显示
def file = inputs.files.singleFile
println file.text
}
}

task IOFinishTask() {
dependsOn writeTask, zreadTask
doLast {
println("io finish")
}
}

以上定义了三个Task:

  • writeTask:写入release.xml,出参为release.xml路径
  • zreadTask:读取release.xml,此时zreadTaskwriteTask建立了隐式依赖
  • IOFinishTask:执行需要依赖zreadTaskwriteTask

第一次执行IOFinishTask时,会执行zreadTaskwriteTask,再次执行时,writeTask标记为up-to-date,表示为增量构建

Task-Input

Task-Input有三种形式的输入:

  • 简单值:包括数值(int)、字符串(String)和任何实现Serializable的类。 @Input
  • 文件:包括单个文件或文件目录 @InputFile @InputDirectory @InputFiles
  • 嵌套对象:非以上两种输入,内部含有嵌套的inputsoutputs类型 @Nested

Task-Output

Task-Output输出主要为:

  • 文件:包括文件或文件目录 @OutputFile @OutputDirectory

运行时API

可以通过Runtime API在自定义Task时使用增量构建。相比与自定义Task类可以减少改动成本。

Runtime API主要有三个:

  • Task.getInputs
    • property/proprtrties == @Input
    • file/files == @InputFile/@InputFiles
    • dir == @InputDirectory
  • Task.getOutputs
    • files/file = @OutputFiles/@OutputFile
    • dirs/dir = @OutputDirectories/@OutputDirectory
  • Task.getDestroyables
1
2


输入校验

Gradle会默认对@InputFile、@InputDirectory、@OutputDirectory进行为空校验,若需要变为可选需要使用@Optional

Task Other

onlyIf断言

onlyIf接收一个闭包作为参数,若闭包中返回true则执行任务,否则跳过该任务(SKIPPED)。主要用于控制任务的执行场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
task testOnlyIf{
println "setOnlyIf"
doLast{
println "testOnlyIf run "
}
}

testOnlyIf.onlyIf{
if(project.hasProperty("skip")){
!project.property("skip")
}
false
}

命令行中输入./gradlew testOnlyIf -Pskip=true则提示Task :testOnlyIf SKIPPED。设置-Pskip=false则输出testOnlyIf run

命令行中-P表示为Project指定K-V格式的属性键值对,使用格式为-PK=V

finalizer任务

监听任务结束状态,可以在结束后执行其他任务

1
2
3
4
5
6
7
8
9
10
11
12
13
task taskx {
doLast{
println "taskx"
}
}

task tasky {
doLast{
println "tasky"
}
}

taskx.finalizedBy tasky

./gradlew taskx -q运行结果为

1
2
3
4
5
6
7
> Task :plugin:taskx
taskx

> Task :plugin:tasky
tasky

taskx执行完毕就会执行tasky

Finalizer即使运行过程中出现异常也不会影响到后续任务的执行,只有一种情况下会出现无法执行后续任务。当前置任务根本没有执行时,不会触发后续任务执行。

判断正在执行的任务

监听正在执行的任务,触发需要监听的任务时,执行功能

1
2
3
4
5
6
7
8
9
10
11
tasks.all {
if("uploadArchives".equalsIgnoreCase(it.name)){
it.doFrist{
//触发任务前执行
}

it.doLast{
//触发任务后执行
}
}
}

挂载自定义Task在构建过程

在其他Task执行过程中,调用自定义Taskexecute

1
2
3
4
5
6
7
8
9
task A{
...
}

task B{
doFirst{
A.execute()
}
}

引用

Gradle官方文档

Gradle增量构建


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!