Gradle 增量构建与构建缓存:自定义 Task 如何实现 “只构建变化内容”?
要实现自定义 Gradle Task 的“只构建变化内容”(即增量构建),核心是通过输入(Inputs)和输出(Outputs)的声明,让 Gradle 能够追踪任务的依赖和结果变化。结合构建缓存(Build Cache),还能实现跨机器/跨构建的结果复用。以下是具体实现步骤和原理:
一、核心原理:输入输出追踪
Gradle 增量构建的核心逻辑是:
- 若 Task 的输入未发生变化,且输出未被修改或删除,则跳过该任务(UP-TO-DATE)。
- 若输入变化或输出丢失,才重新执行任务。
因此,自定义 Task 需明确声明:
- 输入(Inputs):影响任务结果的所有因素(如文件、参数、依赖等)。
- 输出(Outputs):任务生成的所有结果(如文件、目录等)。
二、自定义 Task 实现增量构建
1. 基础实现(继承 DefaultTask)
自定义 Task 需继承 DefaultTask,并通过注解声明输入输出。
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 {// 声明输入:文本内容(会影响输出结果)@Inputabstract Property<String> getInputText()// 声明输出:生成文件的目录(任务结果存放位置)@OutputDirectoryabstract DirectoryProperty getOutputDir()@TaskActionvoid execute() {// 任务逻辑:将输入文本写入输出目录的文件def outputFile = outputDir.file("result.txt").get().asFileoutputFile.parentFile.mkdirs()outputFile.text = inputText.get()println("执行任务:生成文件 ${outputFile.absolutePath}")}
}
2. 注册任务并配置
在 build.gradle 中注册任务并设置输入输出:
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 声明:
abstract class FileProcessingTask extends DefaultTask {// 输入文件(内容变化会触发任务重新执行)@InputFileabstract RegularFileProperty getSourceFile()// 输出文件@OutputFileabstract RegularFileProperty getDestFile()@TaskActionvoid process() {def content = sourceFile.get().asFile.textdestFile.get().asFile.text = content.toUpperCase() // 转换为大写}
}// 注册任务
tasks.register('processFile', FileProcessingTask) {sourceFile.set(file("src/data.txt"))destFile.set(file("$buildDir/processed.txt"))
}
四、启用构建缓存(跨构建复用)
通过构建缓存,可复用其他机器或之前构建的任务结果(无需重新执行)。
1. 全局启用构建缓存
在 settings.gradle 中配置:
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 注解:
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 清晰掌握任务的输入输出依赖关系。
