要实现自定义 Gradle Task 的"只构建变化内容"(即增量构建),核心是通过输入(Inputs)和输出(Outputs)的声明,让 Gradle 能够追踪任务的依赖和结果变化。结合构建缓存(Build Cache),还能实现跨机器/跨构建的结果复用。以下是具体实现步骤和原理:
一、核心原理:输入输出追踪
Gradle 增量构建的核心逻辑是:
- 若 Task 的输入未发生变化 ,且输出未被修改或删除,则跳过该任务(UP-TO-DATE)。
- 若输入变化或输出丢失,才重新执行任务。
因此,自定义 Task 需明确声明:
- 输入(Inputs):影响任务结果的所有因素(如文件、参数、依赖等)。
- 输出(Outputs):任务生成的所有结果(如文件、目录等)。
二、自定义 Task 实现增量构建
1. 基础实现(继承 DefaultTask)
自定义 Task 需继承 DefaultTask,并通过注解声明输入输出。
groovy
import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
// 自定义任务:将输入文本写入输出目录的文件
abstract class MyIncrementalTask extends DefaultTask {
// 声明输入:文本内容(会影响输出结果)
@Input
abstract Property<String> getInputText()
// 声明输出:生成文件的目录(任务结果存放位置)
@OutputDirectory
abstract DirectoryProperty getOutputDir()
@TaskAction
void execute() {
// 任务逻辑:将输入文本写入输出目录的文件
def outputFile = outputDir.file("result.txt").get().asFile
outputFile.parentFile.mkdirs()
outputFile.text = inputText.get()
println("执行任务:生成文件 ${outputFile.absolutePath}")
}
}
2. 注册任务并配置
在 build.gradle 中注册任务并设置输入输出:
groovy
tasks.register('myTask', MyIncrementalTask) {
inputText.set("Hello, Incremental Build!") // 设置输入文本
outputDir.set(file("$buildDir/myOutput")) // 设置输出目录
}
3. 验证增量效果
- 首次执行
./gradlew myTask:任务执行(输出"执行任务...")。 - 再次执行
./gradlew myTask:输入未变,输出存在,任务标记为UP-TO-DATE(跳过执行)。 - 修改输入(如
inputText.set("新内容"))或删除输出目录:任务重新执行。
三、进阶:复杂输入输出声明
根据任务逻辑,需使用不同注解声明输入输出:
| 注解 | 用途 | 示例场景 |
|---|---|---|
@Input |
简单类型输入(字符串、数字、布尔等) | 配置参数、版本号 |
@InputFile |
单个输入文件 | 依赖的源文件 |
@InputDirectory |
输入目录(包含多个文件) | 源文件目录 |
@InputFiles |
多个输入文件/目录(文件集合) | 分散的源文件集合 |
@OutputFile |
单个输出文件 | 生成的单个目标文件 |
@OutputDirectory |
输出目录(包含多个文件) | 生成的目标文件目录 |
@Nested |
嵌套对象(对象内的字段需声明输入) | 复杂配置对象 |
示例:依赖文件的增量任务
若任务依赖某个文件的内容,需用 @InputFile 声明:
groovy
abstract class FileProcessingTask extends DefaultTask {
// 输入文件(内容变化会触发任务重新执行)
@InputFile
abstract RegularFileProperty getSourceFile()
// 输出文件
@OutputFile
abstract RegularFileProperty getDestFile()
@TaskAction
void process() {
def content = sourceFile.get().asFile.text
destFile.get().asFile.text = content.toUpperCase() // 转换为大写
}
}
// 注册任务
tasks.register('processFile', FileProcessingTask) {
sourceFile.set(file("src/data.txt"))
destFile.set(file("$buildDir/processed.txt"))
}
四、启用构建缓存(跨构建复用)
通过构建缓存,可复用其他机器或之前构建的任务结果(无需重新执行)。
1. 全局启用构建缓存
在 settings.gradle 中配置:
groovy
buildCache {
local {
enabled = true // 启用本地缓存
directory = file("$rootDir/.gradle/build-cache") // 缓存目录
}
// 可选:启用远程缓存(如S3、HTTP服务器)
// remote(HttpBuildCache) {
// url = uri("http://example.com/cache")
// enabled = true
// }
}
2. 允许任务参与缓存
在自定义 Task 中添加 @CacheableTask 注解:
groovy
import org.gradle.api.tasks.CacheableTask
@CacheableTask // 标记任务可缓存
abstract class MyIncrementalTask extends DefaultTask {
// ... 输入输出声明同上 ...
}
3. 验证缓存效果
- 首次执行任务:生成结果并写入缓存。
- 清除输出目录(
rm -rf build)后再次执行:Gradle 从缓存中恢复结果(输出FROM-CACHE)。
五、注意事项
-
输入输出必须完整声明:
- 遗漏输入:Gradle 无法感知变化,可能导致旧结果被复用(错误)。
- 遗漏输出:Gradle 可能误判任务未生成结果,重复执行。
-
避免动态输入:
- 输入应是可序列化的、可比较的(如避免使用
new Date()作为输入,可改为固定版本号)。
- 输入应是可序列化的、可比较的(如避免使用
-
目录输出的特殊性:
@OutputDirectory会追踪目录内所有文件的变化,删除文件也会触发任务重新执行。
-
嵌套对象处理:
- 若输入是自定义对象,需用
@Nested注解,并在对象内部为字段声明@Input等注解。
- 若输入是自定义对象,需用
通过以上方式,自定义 Task 可实现"只构建变化内容",并利用构建缓存进一步提升效率。核心是让 Gradle 清晰掌握任务的输入输出依赖关系。