【Gradle-7】Gradle构建核心之Task指南

1、前言

本文是Gradle系列的第7篇,给大家带来Gradle构建核心Task相关的知识点。

2、Gradle中的Task是什么

Task是一个任务,是Gradle中最小的构建单元
Gradle构建的核心就是由Task组成的有向无环图:
image.png
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的几种方式:

  1. register(String name, Action<? super Task> configurationAction)
  2. register(String name, Class type, Action<? super T> configurationAction)
  3. register(String name, Class type)
  4. register(String name, Class type, Object… constructorArgs)
  5. register(String name)

比较常用的是1和2。

  • configurationAction指的是Action,也就是该Task的操作,会在编译时执行;
  • type类型指的是Task类型,可以是自定义类型,也可以指定自带的Copy、Delete、Zip、Jar等类型;

我们可以直接在build.gradle文件中创建一个task:

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的输出没有改变。

分几种情况:

  1. 输入和输出都没有改变;
  2. 输出没有改变;
  3. Task没有操作,有依赖,但依赖的内容是最新的,或者跳过了,或者复用了;
  4. 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,双击即可执行。
截屏2023-06-18 00.26.56.png

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的输出。
image.png
输入的一个重要特征是,它会影响一个或多个输出,如上图,根据源文件的内容和您要运行代码的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文件夹一份了。
WX20230619-011700.png
此时的目录结构:

├── 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())
        }
    }

}

这里的改动分为两步:

  1. 给from属性增加@Incremental注解,表示增量输入属性;
  2. 重写了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文档中列出的任何属性类型注释一起使用。此注释禁用对相应属性的验证检查。有关更多详细信息请参阅验证部分

更多可查看文档

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继承自TaskCollectionNamedDomainObjectCollection,又增加了两个方法可以使用:

  • 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插件

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

https://github.com/yechaoa/GradleX

14、相关文档

猜你喜欢

转载自blog.csdn.net/yechaoa/article/details/131368728