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.name、project.version、project.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.register 或 tasks.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"))
}
嗯,这里要注意:register 和 create 的区别。前者是「注册但不立即创建」,后者是「立即创建」。如果你在配置阶段就要访问这个 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 按特定顺序执行。这时候用 mustRunAfter 或 shouldRunAfter。
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 知识体系总览
说了这么多,咱们用一张图把这一章的核心逻辑串起来。你一看就明白了:
避坑指南:我曾经在项目里看到有人这样写:
tasks.register("clean") {
doLast { delete("build") }
}
结果和 Gradle 内置的 clean Task 冲突了。记住:不要覆盖 Gradle 内置 Task 的名称。如果你要自定义清理逻辑,用 tasks.named("clean") { } 去配置它,而不是重新注册。
好了,这一章的内容就这些。Project 和 Task 是 Gradle 的骨架,搞懂了它们,后面的插件开发、生命周期管理都会轻松很多。记住一句话:Project 提供上下文,Task 提供行为。把这两者的关系理清楚,你的构建脚本就不会乱。