1、前言
本文是Gradle系列的第7篇,给大家带来Gradle构建核心Task
相关的知识点。
2、Gradle中的Task是什么
Task
是一个任务,是Gradle
中最小的构建单元
。
Gradle构建的核心就是由Task组成的有向无环图:
Task是Gradle构建的核心对象,Task可以接收输入、执行某些操作,并根据执行的操作产生输出。
Task管理了一个Action
的List,你可以在List前面插入一个Action(doFirst),也可以从list后面插入(doLast),Action是最小的执行单元
。
3、怎么创建一个task
3.1、Task写在哪
首先想一下Task写在哪?
我们在Gradle系列的第4篇生命周期中介绍到,有三个阶段,第一个阶段初始化会决定哪些项目参与编译,第二个阶段就是解析配置,会生成Task注册表(DAG),第三个阶段就是依次执行Task。
反向来推,执行Task需要一个Task注册表,Task的来源需要先决定哪些项目参与编译,也就是说Task注册表是由参与编译的项目决定的,即可以理解为Task是由Project
对象决定的,所以Task的创建是在Project中,一个build.gradle文件对应一个Project对象,所以我们可以直接在build.gradle文件中创建Task。
只要运行的上下文在Project中就可以。
3.2、创建Task
创建Task需要使用TaskContainer的register方法。
register的几种方式:
- register(String name, Action<? super Task> configurationAction)
- register(String name, Class type, Action<? super T> configurationAction)
- register(String name, Class type)
- register(String name, Class type, Object... constructorArgs)
- register(String name)
比较常用的是1和2。
- configurationAction指的是Action,也就是该Task的操作,会在编译时执行;
- type类型指的是Task类型,可以是自定义类型,也可以指定自带的Copy、Delete、Zip、Jar等类型;
tasks.register("yechaoa") {
println("Task Name = " + it.name)
}
上面调用的就是register的方式1,最后一个参数如果是闭包,可以写在参数外面。
上面task我们是通过TaskContainer(tasks)创建的,在Project对象中也提供了创建Task的方法,写法上有一点差异:
task("yechaoa") {
println "aaa"
}
通过Plugin的方式也可以创建Task,重写的apply方法会有Project对象。
4、如何执行Task
4.1、执行单个Task
命令:
./gradlew taskname
示例:
tasks.register("yechaoa") {
println("Task Name = " + it.name)
}
执行:
./gradlew yechaoa
输出:
Task Name = yechaoa
4.2、执行多个Task
./gradlew taskname taskname taskname
task之间用空格分隔。
4.3、Task同名
如果有两个同名的Task,则会编译失败,即InvalidUserDataException。
* What went wrong:
A problem occurred evaluating project ':app'.
> Cannot add task 'yechaoa' as a task with that name already exists.
4.4、Task执行结果
我们经常会在编译时看到Task后面有一个标签,它表示Task的执行结果。
> Task :app:createDebugVariantModel UP-TO-DATE
> Task :app:preBuild UP-TO-DATE
> Task :app:preDebugBuild UP-TO-DATE
> Task :app:mergeDebugNativeDebugMetadata NO-SOURCE
> Task :app:compileDebugAidl NO-SOURCE
> Task :app:compileDebugRenderscript NO-SOURCE
> Task :app:generateDebugBuildConfig UP-TO-DATE
.....
4.4.1、EXCUTED
表示Task执行,常见。
4.4.2、UP-TO-DATE
> Task :app:preBuild UP-TO-DATE
它表示Task的输出没有改变。
分几种情况:
-
输入和输出都没有改变;
-
输出没有改变;
-
Task没有操作,有依赖,但依赖的内容是最新的,或者跳过了,或者复用了;
-
Task没有操作,也没有依赖。
4.4.3、FOME-CACHE
字面意思,表示可以从缓存中复用上一次的执行结果。
4.4.4、SKIPPED
字面意思,表示跳过。
比如被排除:
$ gradle dist --exclude-task yechaoa
4.4.5、NO-SOURCE
Task不需要执行。有输入和输出,但没有来源。
5、Task的Action
5.1、Action
Task的Action就是编译时所需的操作,它不是一个,它是一组,即可以有多个。
多个Task一般是我们在自定义的时候使用。
5.1.1、自定义Task
class YechaoaTask extends DefaultTask {
@Internal
def taskName = "default"
@TaskAction
def MyAction1() {
println("$taskName -- MyAction1")
}
@TaskAction
def MyAction2() {
println("$taskName -- MyAction2")
}
}
-
自定义一个类,继承自DefaultTask;
-
Action的方法需要添加@TaskAction注解;
-
对外暴露的参数需要使用@Internal注解;
使用自定义Task:
tasks.register("yechaoa", YechaoaTask) {
taskName = "我是传入的Task Name "
}
类型传入自定义Task类。
执行结果:
> Task :app:yechaoa
我是传入的Task Name -- MyAction1
我是传入的Task Name -- MyAction2
如果是Action方法的构造函数传参,参数写在type类型后面即可:
tasks.register('yechaoa', YechaoaTask, 'xxx')
5.2、doFirst
属于Action的一种,在Task Action的头部执行。可以有多个。
5.3、doLast
属于Action的一种,在Task Action的尾部执行。可以有多个。
示例代码:
tasks.register("yechaoa") {
it.doFirst {
println("${it.name} = doFirst 111")
}
it.doFirst {
println("${it.name} = doFirst 222")
}
println("Task Name = " + it.name)
it.doLast {
println("${it.name} = doLast 111")
}
it.doLast {
println("${it.name} = doLast 222")
}
}
执行结果:
Task Name = yechaoa
> Task :app:yechaoa
yechaoa = doFirst 222
yechaoa = doFirst 111
yechaoa = doLast 111
yechaoa = doLast 222
Task Name的输出是在Gradle生命周期的配置阶段,因为它就在闭包下面,不在任何Action里,没有执行时机,配置阶段解析到这个Task就会执行println。
其他输出都是在Task :app:yechaoa下,因为有明确的Action执行时机。
5.4、Action执行顺序
Action的执行顺序也是多数人的一个误区,可以通过上面的日志,我们梳理一下:
-
doFirst:倒序
-
Action:正序
-
doLast:正序
6、Task属性
Task的属性有以下几个:
String TASK_NAME = "name";
String TASK_DESCRIPTION = "description";
String TASK_GROUP = "group";
String TASK_TYPE = "type";
String TASK_DEPENDS_ON = "dependsOn";
String TASK_OVERWRITE = "overwrite";
String TASK_ACTION = "action";
属性配置比较好理解,顾名思义。
如果你的IDEA带了Gradle可视化管理的话,比如Android Studio,这样就可以在右侧Gradle面板菜单中找到我们自定义的Task,双击即可执行。
7、Task依赖
Gradle默认已经有一套Task的构建流程了,你如果想在这个流程加入自定义Task或者在某个自带Task的前后做些切面编程的事,那就需要对Task的依赖关系有所了解。
7.1、dependsOn
tasks.register("yechaoa111") {
it.configure {
dependsOn(provider {
tasks.findAll {
it.name.contains("yechaoa222")
}
})
}
it.doLast {
println("${it.name}")
}
}
tasks.register("yechaoa222") {
it.doLast {
println("${it.name}")
}
}
执行:
./gradlew yechaoa111
输出:
> Task :app:yechaoa222
yechaoa222
> Task :app:yechaoa111
yechaoa111
定义了dependsOn的Task yechaoa111在目标Task yechaoa222之后执行。
其实相对于上面的写法,dependsOn更常见的写法是这种:
def yechaoa111 = tasks.register("yechaoa111") {
it.doLast {
println("${it.name}")
}
}
def yechaoa222 = tasks.register("yechaoa222") {
it.doLast {
println("${it.name}")
}
}
yechaoa111.configure {
dependsOn yechaoa222
}
dependsOn依赖的Task可以是名称也可以是path,也可以是一个type类型:
dependsOn tasks.withType(Copy)
如果是其他项目(Project)的task也可以:
dependsOn "project-lib:yechaoa"
7.2、finalizedBy
为Task添加指定的终结器任务。也就是指定下一个执行的Task,dependsOn指定的是上一个。
task taskY {
finalizedBy "taskX"
}
这里表示taskY执行之后执行taskX。
如果finalizedBy换成dependsOn,则表示taskY执行前要先执行taskX。
7.3、mustRunAfter
def yechaoa111 = tasks.register("yechaoa111") {
it.doLast {
println("${it.name}")
}
}
def yechaoa222 = tasks.register("yechaoa222") {
it.doLast {
println("${it.name}")
}
}
yechaoa111.configure {
mustRunAfter yechaoa222
}
执行:
./gradlew yechaoa111
输出:
> Task :app:yechaoa111
yechaoa111
可以看到yechaoa222并没有执行,因为我们是单独执行的yechaoa111,要查看依赖关系得一起执行才能看出先后顺序。
我们再来一起执行一次:
./gradlew yechaoa111 yechaoa222
输出:
> Task :app:yechaoa222
yechaoa222
> Task :app:yechaoa111
yechaoa111
可以看到mustRunAfter生效了,yechaoa111在yechaoa222之后执行。
7.4、shouldRunAfter
yechaoa111.configure {
shouldRunAfter yechaoa222
}
shouldRunAfter与mustRunAfter的写法一致。
mustRunAfter是「必须运行」,shouldRunAfter是「应该运行」。
如taskB.mustRunAfter(taskA),当taskA和taskB同时运行时,则taskB必须始终在taskA之后运行。
shouldRunAfter规则类似,但不太一样,因为它在两种情况下会被忽略。首先,如果使用该规则会引入一个排序周期;其次,当使用并行执行时,除了“应该运行”任务外,任务的所有依赖项都已满足,那么无论其“应该运行”依赖项是否已运行,都将运行此任务。
8、跳过Task
Gradle提供了多钟跳过Task的方法。
- 条件跳过
- 异常跳过
- 禁用跳过
- 超时跳过
8.1、条件跳过
Gradle提供了onlyIf(Closure onlyIfClosure)方法,只有闭包的结果返回True时,才执行Task。
tasks.register("skipTask") { taskObj ->
taskObj.configure {
onlyIf {
def provider = providers.gradleProperty("yechaoa")
provider.present
}
}
taskObj.doLast {
println("${it.name} is Executed")
}
}
执行:
./gradlew skipTask -Pyechaoa
输出:
> Task :app:skipTask
skipTask is Executed
只有在脚本命令里面加上了-Pyechaoa参数,才会执行skipTask。
举一反三,只要onlyIf闭包结果为True即可,条件自定。
8.2、异常跳过
如果onlyIf不满足需求,也可以使用StopExecutionException来跳过。
StopExecutionException属于异常,当抛出异常的时候,会跳过当前Action及后续Action,即跳过当前Task执行下一个Task。
tasks.register("skipTask") { taskObj ->
taskObj.doFirst {
println("${it.name} is Executed doFirst")
}
taskObj.doLast {
def provider = providers.gradleProperty("yechaoa")
if (provider.present) {
throw new StopExecutionException()
}
println("${it.name} is Executed doLast")
}
}
输出:
> Task :app:skipTask
skipTask is Executed doFirst
如果脚本命令里面加上了-Pyechaoa参数,则会抛异常跳过。
但是该Task中之前的Action会被执行到,比如示例中的doFirst。
8.3、禁用跳过
每个Task 都有一个enabled开关,true开启,false禁用,禁用之后任何操作都不会被执行。
tasks.register("skipTask") { taskObj ->
taskObj.configure {
enabled = true
}
taskObj.doLast {
println("${it.name} is Executed")
}
}
8.4、超时跳过
Task提供了timeout属性用于限制执行时间。
如果Task的运行时间超过指定的时间,则执行该任务的线程将被中断。
默认情况下,任务从不超时。
示例:
tasks.register("skipTask") { taskObj ->
taskObj.configure {
timeout = Duration.ofSeconds(10)
}
taskObj.doLast {
Thread.sleep(11 * 1000)
println("${it.name} is Executed")
}
}
输出:
> Task :app:skipTask FAILED
Requesting stop of task ':app:skipTask' as it has exceeded its configured timeout of 10s.
---Gradle:buildFinished 构建结束了
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':app:skipTask'.
> Timeout has been exceeded
执行异常了,并提示timeout of 10s,因为我在doLast中sleep了11s。
9、Task增量构建
增量构建
是当Task的输入和输出没有变化时,跳过action的执行,当Task输入或输出发生变化时,在action中只对发生变化的输入或输出进行处理,这样就可以避免一个没有变化的Task被反复构建,当Task发生变化时也只处理变化部分,这样可以提高Gradle的构建效率,缩短构建时间。
任何构建工具的一个重要部分是能够避免做已经完成的工作。编译源文件后,除非发生影响输出的某些变化,例如修改源文件或删除输出文件,否则无需重新编译它们。编译可能需要大量时间,因此在不需要时跳过步骤可以节省大量时间。
Gradle提供这种开箱即用的增量构建的功能,当你在编译时,Task在控制台输出中标记为UP-TO-DATE
,这意味着增量构建正在工作。
下面来看看增量构建是如何工作的以及如何确保Task支持增量运行。
9.1、输入和输出
一般情况下,任务需要一些输入并生成一些输出。我们可以将Java编译过程视为Task的一个示例,Java源文件作为Task的输入,而生成的类文件,即编译的结果,是Task的输出。
输入的一个重要特征是,它会影响一个或多个输出,如上图,根据源文件的内容和您要运行代码的Java运行时的最低版本,会生成不同的字节码。
编写Task时,需要告诉Gradle哪些Task属性是输入,哪些是输出。 如果Task属性影响输出,请务必将其注册为输入,否则当它不是时,Task将被视为最新状态。相反,如果属性不影响输出,请不要将属性注册为输入,否则Task可能会在不需要时执行。还要注意可能为完全相同的输入生成不同输出的非确定性Task,这些Task不应配置为增量构建,因为UP-TO-DATE检查将不起作用。
上面的两段理论摘自官方,对于新手来说,可能有点晦涩难懂,下面会带大家实操一下。
9.2、增量构建的两种形式
-
第一种,Task完全可以复用,输入和输出都没有任何变化,即UP-TO-DATE。
-
第二种,有部分变化,只需要针对变化的部分进行操作。
9.3、案例实操
场景:编写一个复制文件的Task,并支持增量构建。
class CopyTask extends DefaultTask {
// 指定输入
@InputFiles
FileCollection from
// 指定输出
@OutputDirectory
Directory to
// task action 执行
@TaskAction
def execute() {
File file = from.getSingleFile()
if (file.isDirectory()) {
from.getAsFileTree().each {
copyFileToDir(it, to)
}
} else {
copyFileToDir(from, to)
}
}
/**
* 复制文件到文件夹
* @param src 要复制的文件
* @param dir 接收的文件夹
* @return
*/
private static def copyFileToDir(File src, Directory dir) {
File dest = new File("${dir.getAsFile().path}/${src.name}")
if (!dest.exists()) {
dest.createNewFile()
}
dest.withOutputStream {
it.write(new FileInputStream(src).getBytes())
}
}
}
在编写Task的时候,我们需要使用注解来声明输入和输出。@InputXXX表示输入,@OutputXXX表示输出。
-
上面代码中from就是我们的输入,即要复制的文件;
-
to是我们的输出,即要接收的文件夹;
-
然后execute()方法就是Task执行的Action。
下面再来看看如何使用。
tasks.register("CopyTask", CopyTask) {
from = files("from")
to = layout.projectDirectory.dir("to")
}
在执行前,造一下数据,在app目录新增一个from的文件夹,并在其下新增一个txt1.txt的文件。
├── app
│ ├── from
│ │ └── txt1.txt
执行:
./gradlew CopyTask
txt1.txt文件已经复制到to文件夹一份了。
此时的目录结构:
├── app
│ ├── from
│ │ └── txt1.txt
│ └── to
│ └── txt1.txt
9.3.1、UP-TO-DATE
刚才执行的日志:
➜ GradleX git:(master) ✗ ./gradlew CopyTask
...
BUILD SUCCESSFUL in 1s
1 actionable task: 1 executed
为了验证增量构建,再执行一次:
➜ GradleX git:(master) ✗ ./gradlew CopyTask
...
BUILD SUCCESSFUL in 1s
1 actionable task: 1 up-to-date
这次我们可以发现,Task的执行结果已经由executed变为up-to-date了,说明我们的增量构建已经生效了。
虽然说此时增量构建已经生效了,但完成度还不够,还需要有颗粒度更细的处理,接着看。
9.3.2、增量构建
上面我们讲了增量构建的两种形式,也已经实现了up-to-date,现在要模拟输入/输出部分变化的场景了。
场景:基于上面的场景,在from文件夹下增加一个txt2.txt文件,并支持增量构建。
增加一个txt2.txt文件再次执行上面的命令时,会发现txt1.txt文件被再次复制了一遍。
这是因为我们的输入有了变化,CopyTask的Action就会全量构建,而我们想要的效果是只复制txt2.txt文件就好了。只对新增或修改的文件做复制操作,没有变化的文件不进行复制。
而要实现这种效果,就得让Action方法支持增量构建。
我们需要给Action方法增加一个InputChanges参数,带InputChanges类型参数的Action方法表示这是一个增量任务操作方法,该参数告诉Gradle,该Action方法仅需要处理更改的输入,此外,Task还需要通过使用 @Incremental或@SkipWhenEmpty来指定至少一个增量文件输入属性。
class CopyTask extends DefaultTask {
// 指定增量输入属性
@Incremental
// 指定输入
@InputFiles
FileCollection from
// 指定输出
@OutputDirectory
Directory to
// task action 执行
@TaskAction
void execute(InputChanges inputChanges) {
boolean incremental = inputChanges.incremental
println("isIncremental = $incremental")
inputChanges.getFileChanges(from).each {
if (it.fileType != FileType.DIRECTORY) {
ChangeType changeType = it.changeType
String fileName = it.file.name
println("ChangeType = $changeType , ChangeFile = $fileName")
if (changeType != ChangeType.REMOVED) {
copyFileToDir(it.file, to)
}
}
}
}
/**
* 复制文件到文件夹
* @param src 要复制的文件
* @param dir 接收的文件夹
* @return
*/
static def copyFileToDir(File file, Directory dir) {
File dest = new File("${dir.getAsFile().path}/${file.name}")
if (!dest.exists()) {
dest.createNewFile()
}
dest.withOutputStream {
it.write(new FileInputStream(file).getBytes())
}
}
}
这里的改动分为两步:
-
给from属性增加@Incremental注解,表示增量输入属性;
-
重写了action方法execute(),增加了InputChanges参数,支持增量复制文件,然后根据文件的ChangeType做校验,只复制新增或修改的文件。
ChangeType的几种类型:
public enum ChangeType {
ADDED,
MODIFIED,
REMOVED
}
-
ADDED:表示文件是新增的;
-
MODIFIED:表示文件是修改的;
-
REMOVED:表示文件被删除;
我们先来执行看看:
./gradlew CopyTask
输出:
> Task :app:CopyTask
isIncremental = false
ChangeType = ADDED , ChangeFile = txt1.txt
第一次执行,并没有增量构建,再执行一次看看。
BUILD SUCCESSFUL in 2s
1 actionable task: 1 up-to-date
第二次直接up-to-date了。
9.3.3、ADDED
这时我们还没有验证到增量构建,我们往from文件夹下增加一个txt2.txt文件,再执行看看。
> Task :app:CopyTask
isIncremental = true
ChangeType = ADDED , ChangeFile = txt2.txt
通过日志可以看出,action的增量构建生效了,并表示txt2.txt是新增的,而txt1.txt文件没有再复制一遍。
此时的目录结构:
├── app
│ ├── build.gradle
│ ├── from
│ │ └── txt1.txt
│ └── txt2.txt
│ └── to
│ ├── txt1.txt
│ └── txt2.txt
9.3.4、MODIFIED
我们还可以进一步验证,在txt1.txt文件里增加一行“yechaoa”模拟修改,再次执行。
> Task :app:CopyTask
isIncremental = true
ChangeType = MODIFIED , ChangeFile = txt1.txt
依然是增量构建,并表示txt1.txt文件是修改的,而txt2.txt文件没有再复制一遍。
9.3.5、REMOVED
在验证一下删除,我们把from文件夹下的txt2.txt文件删除后执行看看。
> Task :app:CopyTask
isIncremental = true
ChangeType = REMOVED , ChangeFile = txt2.txt
依然是增量构建,并表示txt2.txt文件被删除了。
此时的目录结构:
├── app
│ ├── from
│ │ └── txt1.txt
│ └── to
│ ├── txt1.txt
│ └── txt2.txt
可以发现,我们虽然把from文件夹下的txt2.txt文件删除了,Task的Action也确实支持增量构建了,但是to文件夹下的txt2.txt文件还是在的,如果to文件夹的内容会影响到你的构建结果,还是要处理一下保持同步的。
9.4、增量vs全量
Task并不是每次执行都是增量构建,我们可以通过InputChanges的isIncremental方法判断本次构建是否是增量构建,不过有以下几种情况会全量构建:
-
该Task是第一次执行;
-
该Task只有输入没有输出;
-
该Task的upToDateWhen条件返回了false;
-
自上次构建以来,该Task的某个输出文件已更改;
-
自上次构建以来,该Task的某个属性输入发生了变化,例如一些基本类型的属性;
-
自上次构建以来,该Task的某个非增量文件输入发生了变化,非增量文件输入是指没有使用@Incremental或@SkipWhenEmpty注解的文件输入。
当Task处于全量构建时,即InputChanges的isIncremental方法返回false时,通过InputChanges的getFileChanges方法能获取到所有的输入文件,并且每个文件的ChangeType都为ADDED,当Task处于增量构建时,即InputChanges的isIncremental方法返回true时,通过InputChanges的getFileChanges方法能获取到只发生变化的输入文件。
9.5、常用的注解类型
注解 | 类型 | 含义 |
@Input | 任何Serializable类型或依赖性解析结果类型 | 一个简单的输入值或依赖关系解析结果 |
@InputFile | File* | 单个输入文件(不是目录) |
@InputDirectory | File* | 单个输入目录(不是文件) |
@InputFiles | Iterable* | 可迭代的输入文件和目录 |
@OutputFile | File* | 单个输出文件(不是目录) |
@OutputDirectory | File* | 单个输出目录(不是文件) |
@OutputFiles | Map<String, File>*或Iterable | 输出文件的可迭代或映射。使用文件树会关闭任务的缓存。 |
@OutputDirectories | Map<String, File>*或Iterable | 输出目录的可迭代。使用文件树会关闭任务的缓存。 |
@Nested | 任何自定义类型 | 自定义类型,可能无法实现Serializable,但至少有一个字段或属性标记了此表中的注释之一。它甚至可能是另一个@Nested。 |
@Internal | 任何类型 | 表示该属性在内部使用,但既不是输入也不是输出。 |
@SkipWhenEmpty | File或Iterable* | 与@InputFiles或@InputDirectory一起使用,告诉Gradle在相应的文件或目录为空时跳过任务,以及使用此注释声明的所有其他输入文件。由于声明此注释为空的所有输入文件而跳过的任务将导致明显的“无源”结果。例如,NO-SOURCE将在控制台输出中发出。暗示@Incremental。 |
@Incremental | 任何类型 | 与@InputFiles或@InputDirectory一起使用,指示Gradle跟踪对带注释的文件属性的更改,因此可以通过@InputChanges.getFileChanges()查询更改。增量任务需要。 |
@Optional | 任何类型 | 与可选API文档中列出的任何属性类型注释一起使用。此注释禁用对相应属性的验证检查。有关更多详细信息,请参阅验证部分。 |
更多可查看文档。
https://docs.gradle.org/current/userguide/incremental_build.html
9.6、增量构建原理
在首次执行Task之前,Gradle会获取输入的指纹,此指纹包含输入文件的路径和每个文件内容的散列。然后执行Task,如果Task成功完成,Gradle会获取输出的指纹,此指纹包含一组输出文件和每个文件内容的散列,Gradle会在下次执行Task时保留两个指纹。
后续每次在执行Task之前,Gradle都会对输入和输出进行新的指纹识别,如果新指纹与之前的指纹相同,Gradle假设输出是最新的,并跳过Task,如果它们不一样,Gradle会执行Task。Gradle会在下次执行Task时保留两个指纹。
如果文件的统计信息(即lastModified和size)没有改变,Gradle将重复使用上次运行的文件指纹,即当文件的统计信息没有变化时,Gradle不会检测到更改。
Gradle还将Task的代码视为任务输入的一部分,当Task、Action或其依赖项在执行之间发生变化时,Gradle认为该Task是过时的。
Gradle了解文件属性(例如持有Java类路径的属性)是否对顺序敏感,当比较此类属性的指纹时,即使文件顺序发生变化,也会导致Task过时。
请注意,如果Task指定了输出目录,则自上次执行以来添加到该目录的任何文件都会被忽略,并且不会导致Task过时,如此不相关的Task可能会共享一个输出目录,而不会相互干扰,如果出于某种原因这不是你想要的行为,请考虑使用TaskOutputs.upToDateWhen(groovy.lang.Closure)。
另请注意,更改不可用文件的可用性(例如,将损坏的符号链接的目标修改为有效文件,反之亦然),将通过最新检查进行检测和处理。
Task的输入还用于计算启用时用于加载Task输出的构建缓存密钥。
10、查找Task
有时候我们需要找到官方的Task来hook操作,比如加个Action;有时候我们也可以找到自定义的Task让它依赖某个官方Task。
查找Task,主要涉及到TaskContainer对象,顾名思义,Task容器的管理类,它提供了两个方法:
- findByPath(String path),参数可空
- getByPath(String path),参数可空,找不到Task会抛异常UnknownTaskException
同时,TaskContainer继承自TaskCollection
和NamedDomainObjectCollection
,又增加了两个方法可以使用:
- findByName
- getByName
参数定义与xxxByPath方法一样。
10.1、findByName
示例:
def aaa = tasks.findByName("yechaoa").doFirst {
println("yechaoa excuted doFirst by findByName")
}
找到一个名为「yechaoa」的Task,并增加一个doFirst Action,然后在doFirst中打印日志。
我们这时候执行aaa是不会触发yechaoa Task Action的执行,因为并没有依赖关系,所以得执行yechaoa Task。
执行:
./gradlew yechaoa
输出:
> Task :app:yechaoa
yechaoa excuted doFirst by findByName
...
可以看到我们加的日志已经打印出来了。
10.2、findByPath
示例:
def bbb = tasks.findByPath("yechaoa").doFirst {
println("yechaoa excuted doFirst by findByPath")
}
输出:
> Task :app:yechaoa
yechaoa excuted doFirst by findByPath
yechaoa excuted doFirst by findByName
...
10.3、named
通过名称查找Task,如果没有会抛异常UnknownTaskException。
tasks.named("yechaoa") {
it.doFirst {
println("yechaoa excuted doFirst by named")
}
}
输出:
> Task :app:yechaoa
yechaoa excuted doFirst by named
yechaoa excuted doFirst by findByPath
yechaoa excuted doFirst by findByName
...
10.4、其他
withType:
tasks.withType(DefaultTask).configureEach(task -> {
if (task.name.toLowerCase().contains("copytask")) {
println(task.class.name)
}
})
each/forEach/configureEach:
tasks.each {
// do something
}
tasks.forEach(task->{
// do something
})
tasks.configureEach(task -> {
// do something
})
11、番外
11.1、register和create的区别
创建Task除了上面示例中的register
方法,还有create
方法,那它们有什么区别呢?
- 通过register创建时,只有在这个task被需要时,才会创建和配置;
- 通过create创建时,则会立即创建与配置该Task,并添加到TaskContainer中;
直白话就是,register是按需创建task的方式,这样gradle执行的性能更好(并不是你项目的性能)。
create创建task的方式官方已经不推荐了,虽然现在还没标@Deprecated,但未来也可能会被废弃掉。
但是需要注意的是,register属于懒加载,嵌套创建的Task在配置阶段无法被初始化,所以并不会被执行到。
除了register和create,还有一个replace方法,用于替换名称已存在的Task。
11.2、Task Tree
我们可以通过./gradlew tasks来查看所有的Task,但却看不到Task的依赖关系。
要查看Task的依赖关系,我们可以使用task-tree插件。
https://github.com/dorongold/gradle-task-tree
plugins {
id "com.dorongold.task-tree" version "2.1.1"
}
使用时我们只需要在命令后面加上taskTree就行了。
gradle <task 1>...<task N> taskTree
示例:
./gradlew build taskTree
输出:
:app:build
+--- :app:assemble
| +--- :app:assembleDebug
| | +--- :app:mergeDebugNativeDebugMetadata
| | | --- :app:preDebugBuild
| | | --- :app:preBuild
| | --- :app:packageDebug
| | +--- :app:compileDebugJavaWithJavac
| | | +--- :app:compileDebugAidl
| | | | --- :app:preDebugBuild *
| | | +--- :app:compileDebugKotlin
......
12、最后
至此关于Gradle Task的部分就介绍完了。
从Task是什么、写在哪、怎么写、怎么运行、怎么写好等方面为切入点,由浅入深、依次递进的介绍了Task Action执行顺序、自定义Task、Task依赖、Task增量构建等相关知识,总的来说,涉及的知识点还是不少的,更需要在实践中去理解并应用。
希望本文对你有所帮助~
13、Github
14、相关文档
- Authoring Tasks
- TaskContainer
- DefaultTask
- Developing Custom Gradle Task Types
- Incremental build
- Build Script Basics
- Gradle的快速入门学习
- gradle-task-tree
作者:yechaoa
链接:https://juejin.cn/post/7248207744087277605
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。