2. 缓存键生成机制:Task输入属性、输出文件、注解影响、自定义键策略
Gradle 构建缓存的命中判定完全依赖 缓存键(Cache Key) 的唯一性与稳定性。缓存键并非简单地拼接文件路径,而是由 Task 的输入快照、输出文件标识以及注解配置经过规范化后生成的确定性哈希值。本章深入解析缓存键的构成要素、注解对键的影响以及自定义键策略的实现方法,帮助你精准控制缓存命中行为。
2.1 缓存键的核心组成
每个可缓存 Task 的缓存键由以下三部分共同决定:
| 组成部分 | 描述 | 示例 |
|---|---|---|
| 输入属性快照 | Task 所有被注解标记的输入属性(字符串、数字、枚举)的序列化哈希 | @Input String version = "1.2.0" |
| 输入文件/目录快照 | 根据路径敏感度策略计算的文件内容、元数据或相对路径哈希 | @InputFile + @PathSensitive(PathSensitivity.RELATIVE) |
| 输出文件标识 | 输出文件/目录的相对路径及其在任务中的声明顺序(仅用于区分输出) | @OutputDirectory 指向 build/output |
Gradle 并不直接对文件内容整体做哈希——路径敏感度(Path Sensitivity)机制决定了输入快照的精确度,这是影响缓存键差异的核心配置点。
关键原则: 只要任意一个输入属性或输入文件的内容/相对路径发生变化,缓存键就会改变;输出文件的内容变化不会直接影响缓存键,但输出路径的声明顺序影响缓存区分。
2.2 注解类型对缓存键的影响
Gradle 提供多种输入/输出注解,它们对缓存键的计算方式有本质区别:
| 注解 | 作用目标 | 对缓存键的影响 |
|---|---|---|
@Input |
字符串、数字、布尔、序列化对象 | 直接参与哈希,值变化 → 键变化 |
@InputFile / @InputFiles |
单文件 / 文件集合 | 根据 @PathSensitive 计算快照,路径敏感度影响键稳定度 |
@InputDirectory |
目录 | 遍历目录所有文件,结合敏感度生成快照 |
@Nested |
内嵌对象 | 递归处理内嵌对象的所有输入/输出注解,组成子键 |
@OutputFile / @OutputDirectory |
输出文件/目录 | 仅记录路径和顺序声明,不参与哈希值计算 |
常见陷阱: 使用
@InputFiles 但不加 @PathSensitive 时,默认使用 PathSensitivity.ABSOLUTE,导致任何构建位置或临时目录的路径差异都会使缓存键改变,破坏跨机器缓存。
2.3 路径敏感度(Path Sensitivity)策略
通过 @PathSensitive 注解可以控制输入文件快照的粒度,从而平衡缓存准确性与复用性:
| 策略枚举 | 快照内容 | 适用场景 |
|---|---|---|
ABSOLUTE |
文件路径 + 内容哈希 | 需要区分不同工作目录的同一文件 |
RELATIVE |
相对路径(相对于当前项目根目录)+ 内容哈希 | 典型项目构建,跨机器可复用 |
NAME_ONLY |
仅文件名 + 内容哈希 | 输入文件位置变动不影响键时 |
NONE |
仅内容哈希,忽略路径 | 只需内容相关性时(如仅用于校验) |
// build.gradle 示例:自定义 Task 时配置路径敏感度
abstract class MyTransformTask extends DefaultTask {
@InputFile
@PathSensitive(PathSensitivity.RELATIVE)
abstract RegularFileProperty getInputFile()
@OutputDirectory
abstract DirectoryProperty getOutputDir()
@TaskAction
void doTransform() {
// 处理逻辑
}
}
2.4 缓存键生成流程
以下结构图清晰展示了 Gradle 为可缓存 Task 生成缓存键的完整路径:
2.5 自定义键策略
当默认的输入/输出注解组合无法满足业务需求时,Gradle 允许通过以下方式干预缓存键生成:
- 使用
@Input+ 自定义哈希值: 手动计算输入特征的哈希值作为字符串输入,适用于需要排除某些文件内容的场景。 - 覆盖
TaskOutputs.upToDateWhen(): 返回 false 强制 Task 始终重新执行,绕过缓存(不推荐用于生产)。 - 通过
NormalizedPathSensitivity注册自定义规范化器: 在buildSrc或插件中实现PathNormalizer接口,实现复杂路径映射(如忽略临时目录)。
// 自定义策略:忽略 git 仓库中的 .git 目录影响
class GitIgnoringNormalizer : PathNormalizer {
override fun normalize(inputFile: File, relativePath: String): String {
return if (relativePath.contains(".git")) {
// 将 .git 目录的路径归一化为占位符
".git-related"
} else {
relativePath
}
}
}
// 在 task 注册时使用
tasks.register("myTask") {
// 通过 Gradle 的 provider API 注入自定义 normalizer
// 实际配置需要借助 buildService 或 project.objects 实现
}
调试技巧: 在
gradle.properties 中添加 org.gradle.cache.debug=true,然后运行 gradle build --info 查看每个 Task 的缓存键详细日志。日志中会输出 Cache key for task ':xxx' is xxxxx 以及每个输入属性的哈希值。
2.6 常见错误与稳定化方案
| 错误类型 | 表现 | 解决方案 |
|---|---|---|
| 路径污染 | 同一份代码在不同 CI 机器上缓存未命中 | 将 @PathSensitive 改为 RELATIVE 或 NAME_ONLY |
| 时间戳或随机数作为输入 | 每次构建缓存键都不同,无法命中 | 移除 @Input 注解或使用确定性种子替代 |
忘记声明 @OutputDirectory |
Task 被错误标记为不可缓存 | 为输出文件/目录添加正确的输出注解 |
@Nested 对象中包含非缓存属性 |
缓存键异常变化 | 确保内嵌对象中只包含 @Input/@InputFiles 等缓存相关注解 |
稳定缓存键的核心原则:确定性 + 可重复性。所有输入必须在相同输入下产生相同的序列化结果,并且路径敏感度策略应与实际构建环境匹配(开发机、CI 容器、远程构建站)。
2.7 性能优化:减少不必要的快照计算
- 使用
@SkipWhenEmpty: 当输入文件集合为空时跳过缓存计算,减少快照生成开销。 - 精确限定输入文件范围: 避免使用通配符匹配过多无关文件;使用
FileCollection.filter()缩小范围。 - 合理选择
PathSensitivity: 对于不关心路径的文件(如仅需校验内容的中间产物),使用NONE避免路径哈希计算。
// 示例:缩小输入范围并优化快照计算
tasks.register("fastTask") {
inputFile.set(
project.files("src/main/resources/config.json")
.filter { it.name.startsWith("config") }
)
// 使用 NAME_ONLY 忽略目录层次差异
inputFile.get().asFileTree.matching { include("*.json") }
// 设置相对路径敏感度
// 注意:@PathSensitive 注解在任务类中已声明
}
通过掌握缓存键生成机制,你可以在编写自定义 Task 时精确控制缓存范围,避免“缓存污染”导致的失效问题,同时最大化远程缓存的复用率。