任务(Task)详解:创建、依赖、排序、增量与类型

说实话,刚开始用 Gradle 的时候,我最头疼的就是任务这一块。
明明照着文档写了个 task,跑起来却跟预期差得老远。
今天我就把这几个核心点给你掰开揉碎了讲清楚。

1. 任务的创建方式

Gradle 创建任务有几种常见写法。我最早接触的是这种:

task hello {
    doLast {
        println "Hello, Gradle!"
    }
}

后来发现 Groovy DSL 里还能用 tasks.register,这是延迟创建的写法——任务只有被用到时才真正创建。

tasks.register("bye") {
    doLast {
        println "Goodbye!"
    }
}

为什么呢?因为大型项目里任务太多了,全部提前创建会拖慢配置阶段。
我个人习惯:能用 register 尽量用 register,配置阶段能省则省。

创建方式特点推荐场景
task 关键字 + 闭包立即创建,简单直观小型项目或测试脚本
tasks.register()延迟创建,避免无用任务加载多模块、大型项目
tasks.create()立即创建,但已废弃老代码里常见,不建议新用

我的经验:有一次在 50+ 模块的项目里,把所有 task 改成 register 后,配置阶段从 12 秒降到了 3 秒。差别非常明显。

2. 任务依赖

任务之间经常有先后关系。比如你要先编译,再测试,再打包。
Gradle 提供了 dependsOn 来声明依赖。

task compile {
    doLast { println "编译完成" }
}
task test {
    dependsOn compile
    doLast { println "测试完成" }
}
task package {
    dependsOn test
    doLast { println "打包完成" }
}

跑 gradle package 的时候,Gradle 会自动把 compile 和 test 都跑一遍。

但注意:dependsOn 不会强制排序。如果你有两个并行任务都依赖同一个任务,它们会一起等待,但相互之间谁先谁后不一定。
这时就需要用到下面的排序机制。

我曾经掉过的坑:在某个 CI 脚本里,用 dependsOn 声明了 A → B → C,结果打出的包总是缺少 B 生成的资源。排查半天才发现是 B 本身没有直接依赖 A,而是通过另一个任务间接依赖的。最后老老实实把整条链显式写出来了。

3. 任务排序

排序和依赖是两回事。依赖讲究「必须执行」,排序讲究「执行顺序」。
Gradle 提供了两个让你手动干预的方法:

  • taskA.mustRunAfter taskB —— 如果 A 和 B 同时存在,A 必须在 B 之后跑
  • taskA.shouldRunAfter taskB —— 建议在之后,但优先级不是强制的

举个例子:

task lint { doLast { println "代码检查" } }
task checkStyle { doLast { println "风格检查" } }

checkStyle.mustRunAfter lint

如果你同时跑 gradle lint checkStyle,Gradle 会先跑 lint,再跑 checkStyle。
如果只跑其中一个,那排序声明不会生效。

重点:mustRunAfter 不会自动触发依赖关系。它只控制顺序,不强制执行。

4. 增量构建

这个特性我特别喜欢——它让二次构建快得像闪电。
核心思想:输入没变,输出就不重新生成。

Gradle 通过 @Input 和 @Output 注解来识别输入输出。

abstract class MyTask extends DefaultTask {

    @Input
    abstract Property<String> getSourceDir()

    @OutputDirectory
    abstract DirectoryProperty getOutputDir()

    @TaskAction
    void doWork() {
        // 实际的业务逻辑
    }
}

只要 sourceDir 里的内容没变,outputDir 也存在,Gradle 就会跳过这个任务。
而且它会自动对比文件的 lastModified 和 checksum。

注解作用示例
@Input标识输入文件/属性@InputFile, @InputDirectory
@Output标识输出文件/目录@OutputFile, @OutputDirectory
@InputFiles多个输入文件fileTree
@OutputFiles多个输出文件fileCollection

注意:如果你忘了声明 @Output,Gradle 就会认为每次都要执行。我之前接手一个遗留项目,所有自定义任务都没标注输出,每次构建都全量跑,浪费了十几分钟。加上 @Output 后,情况一下就改善了。

5. 任务类型

Gradle 内置了很多任务类型,比如 Copy、Exec、Zip、Delete 等。
直接用这些类型,能省掉很多样板代码。

比如复制文件:

tasks.register("copyFiles", Copy) {
    from "src/main/resources"
    into "build/resources"
    include "**/*.properties"
}

这段代码比手动写 doLast { ... } 干净多了。
而且这些内置类型都自带增量构建支持——因为它们已经标注好了 @Input 和 @Output。

自定义任务类型建议继承 DefaultTask,然后用 @TaskAction 标记执行的方法:

abstract class GenerateReport extends DefaultTask {
    @Input
    abstract Property<String> getReportName()

    @OutputFile
    abstract RegularFileProperty getOutputFile()

    @TaskAction
    void generate() {
        outputFile.get().asFile.write("Report: ${reportName.get()}")
    }
}

tasks.register("genReport", GenerateReport) {
    reportName = "Weekly"
    outputFile = layout.buildDirectory.file("report.txt")
}

我的习惯:能用内置类型(Copy、Zip)就不用自定义。实在需要自定义,一定把 @Input 和 @Output 写全。这样增量构建才能生效。

Gradle 任务核心概念关系图 Gradle 任务核心概念 任务 (Task) 创建方式 task / register 依赖关系 dependsOn 排序 mustRunAfter 增量构建 @Input / @Output 任务类型 Copy / Exec / 自定义 提示 五个概念相互影响 共同决定构建行为

上面这张图把任务相关的五个核心概念串起来了。
创建、依赖、排序、增量、类型——它们之间不是孤立的。
比如你用了 Copy 类型(内置类型),自动就获得了增量支持。
又比如你加了 dependsOn,Gradle 内部会按拓扑排序自动安排执行顺序。

一句话总结

  • 创建:尽量用 register,别浪费配置时间
  • 依赖:显式声明,别偷懒
  • 排序:只在并行执行时用,不替代依赖
  • 增量:标注 @Input/@Output,能获得 10 倍加速
  • 类型:用内置的,少自己写

我自己记这些概念就一个口诀:「创建靠注册,依赖写清楚,排序不滥用,增量要标注,类型选内置」。
你记住这个,日常工作基本够用了。

嗯,任务这块内容确实不少,但掌握了这些,你就能写好和维护好 Gradle 脚本了。
有什么疑问随时翻翻官方文档,或者跑个 gradle help --task 任务名 查看详情。