Gradle缓存状态一致性管理与失效策略详解 - 第4节

4.1 @CacheableTask 注解原理与配置

Gradle 的任务输出缓存机制以 @CacheableTask 注解为入口。该注解标记一个 DefaultTask 子类,声明该任务的输出结果可以被缓存和复用。注意:只对自定义任务有效,不能用于内置任务(如 Copy、Exec)的直接继承。

4.1.1 注解位置与基本规则

位置约束
类级别必须注解在 Task 子类上,不能注解在接口或抽象类上
继承作用子类自动继承父类的 @CacheableTask,但可通过 @DisableCachingByDefault 覆盖
与 inputs/outputs 的关系注解本身不表示缓存策略,必须与 inputs/outputs 声明配合使用

4.1.2 配置示例

// build.gradle.kts
abstract class MyCustomTask : DefaultTask() {

    @get:Input
    abstract val sourceDir: DirectoryProperty

    @get:OutputDirectory
    abstract val outputDir: DirectoryProperty

    @TaskAction
    fun process() {
        val src = sourceDir.asFileTree.files
        val dest = outputDir.get().asFile
        // 处理逻辑
    }
}

// 注册任务
tasks.register<MyCustomTask>("generateDocs") {
    sourceDir.set(layout.projectDirectory.dir("src/docs"))
    outputDir.set(layout.buildDirectory.dir("generated-docs"))
}
关键注意点:只有声明了 @Input / @InputFile / @InputDirectory 和 @OutputFile / @OutputDirectory 等属性的任务才能参与缓存。缺少任一输出声明会导致缓存永久失效或抛出 InvalidUserCodeException。

4.2 inputs/outputs 声明规范

Gradle 通过 inputs 和 outputs 的声明来计算缓存键(cache key)。缓存键由所有输入属性的哈希值以及输出路径的元数据组合而成。任何遗漏的输入属性都可能导致缓存命中失败或产生错误增量。

4.2.1 输入注解分类

注解适用类型哈希方式
@InputString、Number、Boolean 等简单类型值的 toString().hashCode()
@InputFileRegularFileProperty 或 Provider<File>文件内容的哈希(默认 SHA-256)
@InputDirectoryDirectoryProperty目录下所有文件的路径 + 内容哈希树
@Nested包含子 input 属性的对象递归计算所有子属性的哈希
@Classpath类路径集合按顺序计算每个条目的哈希,并记录顺序变化
@Optional可与以上搭配值为空时忽略该属性对缓存键的影响

4.2.2 输出注解分类

注解标识含义
@OutputFile表示单个输出文件,缓存时保存文件副本
@OutputDirectory表示整个输出目录,缓存时保存目录快照
@OutputFiles集合类型的输出文件,如 MapProperty<String, File>
@OutputDirectories集合类型的输出目录
常见错误:使用 @OutputFile 注解一个 DirectoryProperty,或使用 @OutputDirectory 注解一个 RegularFileProperty。这种类型不匹配会导致 Gradle 在构建时直接报错,且缓存完全无法建立。

4.3 up-to-date checks(最新状态检查)

up-to-date checks 是 Gradle 判断任务是否“最新”的本地检查机制,它独立于远程/本地构建缓存。任务只要通过 up-to-date checks,就不会真正执行 @TaskAction 中的代码。其检查顺序如下:

  1. 上次执行记录:检查 .gradle/buildOutputCleanup/cache.properties 中是否存在该任务路径的执行记录。
  2. inputs 哈希比较:重新计算所有 input 属性的哈希值,与上次执行时存储的哈希值对比。
  3. outputs 存在性检查:验证所有输出文件/目录是否存在,且文件大小不为零(零字节文件默认认为无效输出)。
  4. outputs 内容比较:对于非文件的输出,比较上次记录的元数据(如文件数、总大小)。
  5. 构建缓存命中尝试:如果以上检查表明任务不是最新的,并且启用了构建缓存,Gradle 会尝试从缓存加载输出。
性能优化建议:在大型项目中,频繁的 up-to-date checks 会成为性能瓶颈。可以通过 --rerun-tasks 强制忽略缓存重新执行,也可以在开发阶段使用 gradle build --watch-fs 让 Gradle 持久化文件系统状态,减少哈希计算成本。

4.4 完整缓存条件判定流程图

Gradle 任务输出缓存条件判定流程 任务开始执行 @CacheableTask @Input / @Output 声明完整性 上次执行记录存在? 是 否 inputs 哈希对比 outputs 存在性检查 全部检查通过? 否,尝试缓存恢复 是,跳过执行 缓存命中 → 恢复输出 up-to-date ✓ 非最新时尝试缓存恢复

4.5 缓存状态调试命令

当缓存行为与预期不一致时,使用以下命令可以快速定位原因:

# 查看任务缓存状态(跳过/命中/未命中)
gradle build --info | grep "CACHED\|UP-TO-DATE\|EXECUTING"

# 详细输出每个任务的 up-to-date 检查过程
gradle build --debug 2>&1 | grep "CacheCheck\|up-to-date\|inputHash\|outputFile"

# 强制清空本地缓存,验证从头构建
gradle clean build --no-build-cache

# 分析缓存键组成
gradle help --task <taskName> --scan
调试技巧:使用 --scan 生成的构建扫描页面中,可以直观地看到每个任务的缓存状态、inputs 哈希值和输出文件列表,这是排查缓存一致性问题最有效的工具。

4.6 常见错误与排查

错误现象根因解决方法
任务从未被缓存缺少 @Output* 注解或输出路径不在 buildDir 下确保至少有一个输出注解,且输出路径在构建目录内
缓存永远不命中输入属性计算了随机值或路径检查是否将 projectDir 路径作为输入
up-to-date 检查误判输入属性为 @InputFiles 但输出未声明给任务添加正确的 @OutputFile 或 @OutputDirectory
构建缓存被忽略build.gradle 中 buildCache 配置未生效检查 settings.gradle 中 buildCache 段落的 local { enabled = true }
避坑指南:切勿将 @get:OutputFile 与 @get:OutputDirectory 混用在同一路径。如果一个任务既输出文件又输出目录,必须为它们分配不同的路径。否则 Gradle 会认为输出冲突并抛出 DuplicateOutputFile 异常。