自定义 task 必须显式声明 inputs 和 outputs,否则 gradle 无法识别依赖与产物,导致增量构建失效、任务永远无法标记为 up-to-date;声明需通过注解(如 @input/@outputdirectory)在抽象任务类中作用于 getter,或通过 inputs/outputs api 在闭包任务中配置,且输入指纹必须稳定、注册时机须正确。

自定义 Task 必须显式声明 inputs 和 outputs
Gradle 不会自动推断你的 Task 有哪些输入或输出。哪怕逻辑只读一个配置文件、只写一个 JSON,不声明就等于“无输入无输出”,每次执行都强制全量跑,UP-TO-DATE 永远不会出现。
常见错误是只在 doLast 里操作文件,却没用 inputs.file() 或 outputs.dir() 告诉 Gradle:“这些才是我真正依赖和生成的东西”。
-
inputs.file("src/main/config.yaml")—— 声明单个文件为输入,内容变更触发重执行 -
inputs.files(fileTree("src/main/scripts").matching { include "**/*.js" })—— 声明一组匹配文件 -
outputs.dir("$buildDir/generated")—— 声明整个目录为输出,目录缺失或任意子文件变更都会导致任务失效 - 避免把无关字段塞进
inputs.property(),比如时间戳、随机数、构建 ID——它们会让指纹总变,彻底废掉增量
@Input/@OutputDirectory 注解只对自定义 Task 类生效
如果你用 tasks.register("myTask", MyCustomTask.class) 方式注册,那必须在 MyCustomTask 类中用注解标记 getter 方法;直接用 tasks.create 写闭包的匿名 Task,注解完全无效。
例如,这个类能支持增量:
abstract class GenerateApiTask : DefaultTask() {
@get:Input
abstract val configPath: Property<string><pre class="brush:java;toolbar:false;">@get:OutputDirectory
abstract val outputDir: DirectoryProperty
@TaskAction
fun generate() { /* ... */ }
}
而下面这种写法,无论你怎么加 @Input 都不起作用:
tasks.create("brokenTask") {
@Input // ❌ 编译报错:注解不能用于闭包内局部变量
val cfg = "config.json"
doLast { /* ... */ }
}
输入指纹必须稳定,否则增量形同虚设
Gradle 对 inputs 计算 SHA-256 指纹。如果输入内容本身不稳定(比如含当前时间、进程 ID、未排序的 Map 序列化结果),哪怕源文件没动,指纹也变,任务永远无法复用。
- 读取 JSON/YAML 配置时,确保解析后结构顺序一致,避免用
HashMap直接转字符串 - 不要把
System.currentTimeMillis()或new Date()当作@Input值 - 若需传动态值(如版本号),用
@Input+Provider组合,并确认该值在两次构建间确实不变 - 文件输入推荐用
inputs.files(...).withPathSensitivity(PathSensitivity.RELATIVE),避免因绝对路径差异误判变更
执行前务必验证 inputs/outputs 是否被正确注册
Gradle 不会在配置阶段报错,但你可以用 --dry-run 或 --debug 看它是否识别到你的声明:
运行 ./gradlew myTask --dry-run,如果输出里没有 Inputs: 和 Outputs: 行,说明声明失败;再加 --debug | grep -A5 "myTask.*input" 可查具体注册日志。
- 检查是否拼错了方法名,比如写了
inputFile而非inputs.file - 确认 Task 实例是在
register(推荐)或create后立即配置,而不是在afterEvaluate里才设置inputs——后者太晚,Gradle 已锁定输入图 - Android 项目中,如果 Task 依赖
androidComponents回调,注意它默认在配置后期注册,inputs/outputs必须在回调内设置,且不能延迟到onVariants外部
最易被忽略的是:增量不是“开了就灵”的开关,它是靠输入输出契约成立才能生效的脆弱机制。少声明一个 inputs.file(),多塞一个不稳定的 @Input,或者用错注册时机,都会让任务退化成每次都全量执行。











