任务最佳实践
避免使用 DependsOn
解释
带有动作的任务应声明其输入和输出,以便 Gradle 的最新性检查可以自动确定何时需要运行或重新运行这些任务。
使用 dependsOn
链接任务是一种粗粒度得多的机制,它**不**允许 Gradle 理解为什么某个任务需要先运行一个前置任务,或者前置任务中的哪些特定文件是需要的。dependsOn
迫使 Gradle 假设当前任务需要前置任务产生的*每一个*文件。这可能导致不必要的任务执行,降低构建性能。
示例
这里有一个任务,将输出写入两个单独的文件
abstract class SimplePrintingTask : DefaultTask() {
@get:OutputFile
abstract val messageFile: RegularFileProperty
@get:OutputFile
abstract val audienceFile: RegularFileProperty
@TaskAction (1)
fun run() {
messageFile.get().asFile.writeText("Hello")
audienceFile.get().asFile.writeText("World")
}
}
tasks.register<SimplePrintingTask>("helloWorld") { (2)
messageFile.set(layout.buildDirectory.file("message.txt"))
audienceFile.set(layout.buildDirectory.file("audience.txt"))
}
abstract class SimplePrintingTask extends DefaultTask {
@OutputFile
abstract RegularFileProperty getMessageFile()
@OutputFile
abstract RegularFileProperty getAudienceFile()
@TaskAction (1)
void run() {
messageFile.get().asFile.write("Hello")
audienceFile.get().asFile.write("World")
}
}
tasks.register("helloWorld", SimplePrintingTask) { (2)
messageFile = layout.buildDirectory.file("message.txt")
audienceFile = layout.buildDirectory.file("audience.txt")
}
1 | 具有多个输出的任务:hellowWorld 任务将其“Hello”打印到 messageFile ,将其“World”打印到 audienceFile 。 |
2 | 注册任务:hellowWorld 产生 "message.txt" 和 "audience.txt" 输出。 |
不要这样做
如果您想使用另一个任务来翻译 message.txt
文件中的问候语,可以这样做
abstract class SimpleTranslationTask : DefaultTask() {
@get:InputFile
abstract val messageFile: RegularFileProperty
@get:OutputFile
abstract val translatedFile: RegularFileProperty
init {
messageFile.convention(project.layout.buildDirectory.file("message.txt"))
translatedFile.convention(project.layout.buildDirectory.file("translated.txt"))
}
@TaskAction (1)
fun run() {
val message = messageFile.get().asFile.readText(Charsets.UTF_8)
val translatedMessage = if (message == "Hello") "Bonjour" else "Unknown"
logger.lifecycle("Translation: " + translatedMessage)
translatedFile.get().asFile.writeText(translatedMessage)
}
}
tasks.register<SimpleTranslationTask>("translateBad") {
dependsOn(tasks.named("helloWorld")) (2)
}
abstract class SimpleTranslationTask extends DefaultTask {
@InputFile
abstract RegularFileProperty getMessageFile()
@OutputFile
abstract RegularFileProperty getTranslatedFile()
SimpleTranslationTask() {
messageFile.convention(project.layout.buildDirectory.file("message.txt"))
translatedFile.convention(project.layout.buildDirectory.file("translated.txt"))
}
@TaskAction (1)
void run() {
def message = messageFile.get().asFile.text
def translatedMessage = message == "Hello" ? "Bonjour" : "Unknown"
logger.lifecycle("Translation: " + translatedMessage)
translatedFile.get().asFile.write(translatedMessage)
}
}
tasks.register("translateBad", SimpleTranslationTask) {
dependsOn(tasks.named("helloWorld")) (2)
}
1 | 翻译任务设置:translateBad 需要先运行 helloWorld 来生成消息文件,否则文件不存在会导致错误。 |
2 | 显式任务依赖:运行 translateBad 会导致 helloWorld 先运行,但 Gradle 不理解原因。 |
请这样做
相反,您应该像这样显式地连接任务输入和输出
tasks.register<SimpleTranslationTask>("translateGood") {
inputs.file(tasks.named<SimplePrintingTask>("helloWorld").map { messageFile }) (1)
}
tasks.register("translateGood", SimpleTranslationTask) {
inputs.file(tasks.named("helloWorld", SimplePrintingTask).map { messageFile }) (1)
}
1 | 注册隐式任务依赖:translateGood 只需要 helloWorld 产生的文件之一。Gradle 现在理解 translateGood 需要 helloWorld 先成功运行,因为它需要创建 message.txt 文件,该文件将由翻译任务使用。Gradle 可以利用这些信息来优化任务调度。使用 map 方法可以避免急切地检索 helloWorld 任务,直到需要其输出才能确定 translateGood 是否应运行时才进行检索。 |
优先使用 @CacheableTask
和 @DisableCachingByDefault
而非 cacheIf(Spec)
和 doNotCacheIf(String, Spec)
cacheIf
和 doNotCacheIf
方法应仅在任务的可缓存性因任务实例不同而异,或在 Gradle 执行任务之前无法确定其可缓存性的情况下使用。对于任何*总是*可缓存的任务,您应该优先使用 @CacheableTask
注解来标注任务类本身。同样,对于某个任务类型的*所有实例*,应使用 @DisableCachingByDefault
来始终禁用缓存。
解释
标注任务类型将确保 Gradle 正确地理解该类型的*每个任务实例*都是可缓存的(或不可缓存的)。这消除了在构建脚本中单独配置每个任务实例的需要。
使用注解还可以*记录*任务类型在其自身源代码中的预期可缓存性,显示在 Javadoc 中,并使任务的行为对其他开发者来说清晰明了,而无需他们检查每个任务实例的配置。这比运行测试来确定可缓存性也稍微更有效率。
请记住,只有生成可重现且可重定位输出的任务才应标记为 @CacheableTask
。
示例
不要这样做
如果您想复用任务的输出,不应该这样做
tasks.register<Delete>("clean") {
delete(layout.buildDirectory)
}
abstract class BadCalculatorTask : DefaultTask() { (1)
@get:Input
abstract val first: Property<Int>
@get:Input
abstract val second: Property<Int>
@get:OutputFile
abstract val outputFile: RegularFileProperty
@TaskAction
fun run() {
val result = first.get() + second.get()
logger.lifecycle("Result: $result")
outputFile.get().asFile.writeText(result.toString())
}
}
tasks.register<BadCalculatorTask>("addBad1") {
first = 10
second = 25
outputFile = layout.buildDirectory.file("badOutput.txt")
outputs.cacheIf { true } (2)
}
tasks.register<BadCalculatorTask>("addBad2") { (3)
first = 3
second = 7
outputFile = layout.buildDirectory.file("badOutput2.txt")
}
tasks.register("clean", Delete) {
delete layout.buildDirectory
}
abstract class BadCalculatorTask extends DefaultTask {
@Input
abstract Property<Integer> getFirst()
@Input
abstract Property<Integer> getSecond()
@OutputFile
abstract RegularFileProperty getOutputFile()
@TaskAction
void run() {
def result = first.get() + second.get()
logger.lifecycle("Result: " + result)
outputFile.get().asFile.write(result.toString())
}
}
tasks.register("addBad1", BadCalculatorTask) {
first = 10
second = 25
outputFile = layout.buildDirectory.file("badOutput.txt")
outputs.cacheIf { true }
}
tasks.register("addBad2", BadCalculatorTask) {
first = 3
second = 7
outputFile = layout.buildDirectory.file("badOutput2.txt")
}
1 | 定义任务:BadCalculatorTask 类型是确定性的,产生可重定位的输出,但未被标注。 |
2 | 将任务实例标记为可缓存:此示例展示了如何将特定的任务实例标记为可缓存。 |
3 | 忘记将任务实例标记为可缓存:不幸的是,BadCalculatorTask 类型的 addBad2 实例未标记为可缓存,因此不会被缓存,尽管其行为与 addBad1 相同。 |
请这样做
由于此任务符合可缓存性标准(我们可以想象在 @TaskAction
中进行更复杂的计算,这将受益于通过缓存实现的自动避免工作),您应该将*任务类型本身*标记为可缓存,如下所示
@CacheableTask (1)
abstract class GoodCalculatorTask : DefaultTask() {
@get:Input
abstract val first: Property<Int>
@get:Input
abstract val second: Property<Int>
@get:OutputFile
abstract val outputFile: RegularFileProperty
@TaskAction
fun run() {
val result = first.get() + second.get()
logger.lifecycle("Result: $result")
outputFile.get().asFile.writeText(result.toString())
}
}
tasks.register<GoodCalculatorTask>("addGood1") { (2)
first = 10
second = 25
outputFile = layout.buildDirectory.file("goodOutput.txt")
}
tasks.register<GoodCalculatorTask>("addGood2") {
first = 3
second = 7
outputFile = layout.buildDirectory.file("goodOutput2.txt")
}
@CacheableTask (1)
abstract class GoodCalculatorTask extends DefaultTask {
@Input
abstract Property<Integer> getFirst()
@Input
abstract Property<Integer> getSecond()
@OutputFile
abstract RegularFileProperty getOutputFile()
@TaskAction
void run() {
def result = first.get() + second.get()
logger.lifecycle("Result: " + result)
outputFile.get().asFile.write(result.toString())
}
}
tasks.register("addGood1", GoodCalculatorTask) {
first = 10
second = 25
outputFile = layout.buildDirectory.file("goodOutput.txt")
}
tasks.register("addGood2", GoodCalculatorTask) { (2)
first = 3
second = 7
outputFile = layout.buildDirectory.file("goodOutput2.txt")
}
1 | 标注任务类型:将 @CacheableTask 应用于任务类型会告知 Gradle,此任务的实例应*始终*被缓存。 |
2 | 无需再做其他事情:当我们注册任务实例时,无需再做其他事情 - Gradle 知道要缓存它们。 |