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 生成缓存键的完整路径:

缓存键生成流程 @Input 属性快照 @InputFiles 文件快照 @InputDirectory 目录快照 @PathSensitive 规范化 @Nested 递归展开 输入规范哈希计算 输出路径标识组合 最终 Cache Key(SHA-256) 输入规范 = 序列化后的 属性值 + 文件快照(含路径敏感度)

2.5 自定义键策略

当默认的输入/输出注解组合无法满足业务需求时,Gradle 允许通过以下方式干预缓存键生成:

  1. 使用 @Input + 自定义哈希值: 手动计算输入特征的哈希值作为字符串输入,适用于需要排除某些文件内容的场景。
  2. 覆盖 TaskOutputs.upToDateWhen(): 返回 false 强制 Task 始终重新执行,绕过缓存(不推荐用于生产)。
  3. 通过 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 时精确控制缓存范围,避免“缓存污染”导致的失效问题,同时最大化远程缓存的复用率。