4、Project与Task:Project API、Task的声明与配置、Task依赖与排序

好,咱们进入第四章。说实话,这一章是整个 Gradle Kotlin DSL 的基石。你想想看,Gradle 里最核心的两个概念是什么?就是 Project 和 Task。Project 是构建的容器,Task 是构建的执行单元。搞懂了这俩,你才算真正入了 Gradle 的门。

我个人习惯把 Project 想象成一个「项目管家」,它知道你的源码在哪、依赖在哪、要打什么包。而 Task 就是管家手底下的「干活的人」——编译、测试、打包、部署,全是 Task 干的。

4.1 Project API:构建脚本的「根」

每个 build.gradle.kts 文件,本质上就是一个 Project 对象的配置脚本。你在脚本里写的所有顶层代码,最终都会委托给 Project 的 API 去执行。

Project API 提供了几个核心能力:

  • 属性访问project.nameproject.versionproject.group
  • 配置方法repositories { }dependencies { }configurations { }
  • Task 容器tasks { } 块,用于声明和配置 Task
  • 文件操作file()files()copy { }

核心要点:在 Kotlin DSL 中,Project 的所有属性和方法都是类型安全的。你写 project.version = "1.0.0",IDE 会直接给你补全,不会出现字符串拼写错误。

我在项目中遇到过一个问题:有人直接在脚本里写 version = "1.0",结果发现子项目里读不到这个版本。为什么?因为 version 默认是 Project 的属性,但如果你在 allprojects { } 块外面赋值,它只对当前 Project 生效。正确的做法是:

// 根项目 build.gradle.kts
allprojects {
    version = "1.0.0"
    group = "com.example"
}

// 子项目可以直接引用
subprojects {
    println("${project.name} 版本: ${project.version}")
}

4.2 Task 的声明与配置

Task 是 Gradle 里最基础的执行单元。声明一个 Task 其实很简单,但 Kotlin DSL 给了我们更优雅的方式。

4.2.1 声明 Task

最直接的方式是用 tasks.registertasks.create。我个人推荐用 register,因为它支持延迟创建,性能更好。

// 声明一个简单的 Task
tasks.register("hello") {
    doLast {
        println("Hello, Gradle!")
    }
}

// 声明带类型的 Task
tasks.register<Zip>("myZip") {
    from("src")
    archiveFileName.set("my.zip")
    destinationDirectory.set(layout.buildDirectory.dir("dist"))
}

嗯,这里要注意:registercreate 的区别。前者是「注册但不立即创建」,后者是「立即创建」。如果你在配置阶段就要访问这个 Task,那就用 create;否则,register 更省资源。

4.2.2 配置 Task

配置 Task 有两种方式:声明时配置和声明后配置。

// 方式一:声明时配置
tasks.register("buildInfo") {
    group = "build"
    description = "输出构建信息"
    doLast {
        println("构建时间: ${System.currentTimeMillis()}")
    }
}

// 方式二:声明后配置(通过名称访问)
tasks.named("buildInfo") {
    // 这里可以追加配置
    doFirst {
        println("开始构建...")
    }
}

小技巧:用 tasks.named 比用 tasks.getByName 更安全。因为 named 返回的是 NamedDomainObjectProvider,支持延迟解析,不会因为 Task 不存在就立刻报错。

我曾经踩过一个坑:在 doLast 里修改了 Task 的输入属性,结果发现下次构建时没生效。后来才明白,doLast 是执行阶段才运行的,而 Task 的输入应该在配置阶段就确定好。所以,配置属性放在 Task 块里,执行逻辑放在 doFirst/doLast

4.3 Task 依赖与排序

Task 之间不是孤立的。编译 Task 必须在测试 Task 之前运行,打包 Task 必须在编译 Task 之后运行。这就是依赖和排序要解决的问题。

4.3.1 显式依赖:dependsOn

最直接的依赖方式是用 dependsOn。它告诉 Gradle:「先跑我依赖的 Task,再跑我」。

tasks.register("taskA") {
    doLast { println("执行 Task A") }
}

tasks.register("taskB") {
    dependsOn("taskA")  // 或者 dependsOn(tasks.named("taskA"))
    doLast { println("执行 Task B") }
}

// 运行 taskB 时,会自动先运行 taskA
// 输出顺序:Task A → Task B

4.3.2 隐式依赖:Task 输入输出

Gradle 还有一个更聪明的机制:通过输入输出自动推断依赖。如果 Task B 的输入是 Task A 的输出,Gradle 会自动建立依赖关系。

tasks.register<Copy>("copyFiles") {
    from("src/data")
    into(layout.buildDirectory.dir("data"))
}

tasks.register<Zip>("zipData") {
    from(layout.buildDirectory.dir("data"))  // 依赖 copyFiles 的输出
    archiveFileName.set("data.zip")
    destinationDirectory.set(layout.buildDirectory.dir("dist"))
}

// 运行 zipData 时,Gradle 会自动先运行 copyFiles

最佳实践:能用输入输出推断依赖的,就别手动写 dependsOn。这样 Gradle 可以更好地做增量构建——如果输入没变,Task 就不执行。

4.3.3 排序:mustRunAfter 和 shouldRunAfter

有时候你不想建立依赖关系,只是想让 Task 按特定顺序执行。这时候用 mustRunAftershouldRunAfter

tasks.register("taskX") {
    doLast { println("Task X") }
}

tasks.register("taskY") {
    mustRunAfter("taskX")  // 如果同时执行,Y 必须在 X 之后
    doLast { println("Task Y") }
}

// 同时运行两个 Task:gradle taskX taskY
// 输出顺序:Task X → Task Y
// 但如果只运行 taskY,不会触发 taskX

mustRunAfter 是强约束,shouldRunAfter 是弱约束。后者在循环依赖或并行执行时可能会被忽略。我个人建议:除非有特殊原因,否则用 mustRunAfter 更稳妥。

4.3.4 依赖与排序的对比

机制 是否触发依赖 Task 适用场景
dependsOn 强依赖,A 不执行 B 就不能执行
mustRunAfter 仅排序,不触发依赖 Task
shouldRunAfter 弱排序,允许 Gradle 优化
输入输出推断 自动建立,推荐优先使用

4.4 知识体系总览

说了这么多,咱们用一张图把这一章的核心逻辑串起来。你一看就明白了:

Project 与 Task 核心知识体系 Project(项目管家) Task 声明与配置 Task 依赖与排序 Project API 属性 register / create named / configure dependsOn mustRunAfter 输入输出推断 属性访问 配置方法 文件操作 类型安全 + 延迟配置 + 增量构建 Kotlin DSL 三大优势

避坑指南:我曾经在项目里看到有人这样写:

tasks.register("clean") {
    doLast { delete("build") }
}

结果和 Gradle 内置的 clean Task 冲突了。记住:不要覆盖 Gradle 内置 Task 的名称。如果你要自定义清理逻辑,用 tasks.named("clean") { } 去配置它,而不是重新注册。

好了,这一章的内容就这些。Project 和 Task 是 Gradle 的骨架,搞懂了它们,后面的插件开发、生命周期管理都会轻松很多。记住一句话:Project 提供上下文,Task 提供行为。把这两者的关系理清楚,你的构建脚本就不会乱。


公众号:蓝海资料掘金营,微信deep3321