4. Gradle Task详解:Task的创建方式、Task的配置与执行、Task依赖与排序、Task的输入输出、增量构建原理

说实话,Task是Gradle构建脚本里最核心的概念。你写的每一行构建逻辑,最终都会落到某个Task上。我刚开始接触Gradle时,总觉得Task就是个「跑脚本」的东西,后来踩了不少坑才明白——Task的设计远比我想象的讲究。

这一章,咱们就把Task彻底聊透。从怎么创建,到怎么配置,再到怎么让它们有序执行,最后聊聊Gradle引以为傲的增量构建。嗯,内容不少,但都是干货。

4.1 Task的创建方式

Task的创建方式有好几种,我个人最常用的是通过tasks.register()。为什么?因为它支持延迟创建,只有Task真正需要执行时才会被实例化。这在大型项目里能明显提升配置阶段的性能。

// 方式一:使用 register(推荐)
tasks.register('hello') {
    doLast {
        println 'Hello, Gradle!'
    }
}

// 方式二:使用 create(立即创建)
tasks.create('helloOld') {
    doLast {
        println 'Hello, old way!'
    }
}

// 方式三:通过已有的 Task 类型
tasks.register('myCopy', Copy) {
    from 'src'
    into 'dest'
}

关键区别register()是延迟创建,create()是立即创建。在配置阶段,register()不会实例化Task对象,只有执行阶段才会。我建议你养成用register()的习惯。

你可能会问:「那我什么时候用create()?」说实话,我只有在写快速验证脚本时才会用create()。生产代码里,一律register()

4.2 Task的配置与执行

Task的生命周期分为两个阶段:配置阶段执行阶段。这个区分特别重要,我见过不少同事把逻辑写错地方,导致每次构建都跑一遍不该跑的代码。

tasks.register('example') {
    // 配置阶段执行 —— 每次构建都会执行
    println '我在配置阶段执行'

    doFirst {
        // 执行阶段执行 —— 只有Task被调用时才执行
        println '我在执行阶段,最先执行'
    }

    doLast {
        // 执行阶段执行 —— 只有Task被调用时才执行
        println '我在执行阶段,最后执行'
    }
}

我曾经犯过一个错误:在配置阶段写了一个耗时的网络请求,结果每次执行gradle tasks都要等好几秒。后来才意识到,应该把耗时操作放到doLast里。

说白了,配置阶段的代码每次构建都会跑,不管你这个Task有没有被执行。执行阶段的代码,只有Task被真正执行时才会跑。这个区别,你品,你细品。

4.3 Task依赖与排序

Task之间经常有依赖关系。比如打APK之前,必须先编译代码。Gradle提供了几种方式来控制Task的执行顺序。

方式 说明 示例
dependsOn 强依赖,先执行依赖的Task taskB.dependsOn(taskA)
mustRunAfter 排序约束,不强制依赖 taskB.mustRunAfter(taskA)
shouldRunAfter 弱排序,可被忽略 taskB.shouldRunAfter(taskA)
finalizedBy 无论成功失败,最后执行 taskA.finalizedBy(taskB)
tasks.register('compile') {
    doLast { println '编译中...' }
}

tasks.register('test') {
    dependsOn 'compile'
    doLast { println '测试中...' }
}

tasks.register('clean') {
    doLast { println '清理中...' }
}

tasks.register('build') {
    dependsOn 'test'
    finalizedBy 'clean'
    doLast { println '构建完成' }
}

这里有个细节:dependsOnmustRunAfter的区别。前者是「我依赖你,你必须先执行」,后者是「如果你也要执行,那你必须在我前面」。你想想看,如果taskAtaskB没有依赖关系,但你想让它们按顺序执行,就用mustRunAfter

4.4 Task的输入输出

Task的输入输出是Gradle增量构建的基础。说白了,Gradle通过比较输入输出的状态,来判断一个Task是否需要重新执行。

abstract class MyTransformTask extends DefaultTask {

    @InputFile
    abstract RegularFileProperty getInputFile()

    @OutputFile
    abstract RegularFileProperty getOutputFile()

    @TaskAction
    void transform() {
        inputFile.get().asFile.copyTo(outputFile.get().asFile)
        println "文件已转换: ${inputFile.get()} -> ${outputFile.get()}"
    }
}

tasks.register('transformFile', MyTransformTask) {
    inputFile.set(file('input.txt'))
    outputFile.set(file('output.txt'))
}

核心注解

  • @InputFile / @InputFiles:标记输入文件
  • @OutputFile / @OutputDirectory:标记输出文件
  • @Input:标记普通输入参数
  • @TaskAction:标记Task执行的方法

我记得有一次,项目构建越来越慢,排查后发现有个Task没有声明输入输出。每次构建都重新执行,哪怕输入文件根本没变。加上注解后,构建时间从40秒降到了5秒。嗯,增量构建的威力就在这里。

4.5 增量构建原理

增量构建,说白了就是「只做必要的,跳过不必要的」。Gradle通过比较Task的输入输出状态,来决定是否跳过执行。

具体原理是这样的:

  1. Gradle会记录每个Task的输入输出「快照」
  2. 下次构建时,重新计算输入输出的快照
  3. 如果快照没变,Gradle就认为Task是UP-TO-DATE,直接跳过
  4. 如果快照变了,Task才会重新执行

小技巧:执行gradle build --info可以看到哪些Task被标记为UP-TO-DATE。如果某个Task明明输入没变却每次都执行,说明你的输入输出声明有问题。

下面这张图展示了增量构建的核心流程:

增量构建核心流程 Task 被触发 检查输入输出快照是否变化 UP-TO-DATE 重新执行 Task 构建完成 快照包括:文件内容哈希、文件大小、最后修改时间等

你可能会问:「Gradle怎么知道输入输出变了?」答案是快照对比。Gradle会计算输入文件的哈希值、文件大小、最后修改时间等信息,生成一个快照。下次构建时,重新计算快照,一对比就知道变没变。

注意:如果你的Task没有声明任何输入输出,Gradle会认为它永远需要执行。这就是为什么有些Task每次构建都跑,明明什么都没变。我曾经接手过一个项目,里面有个Task没加任何注解,每次构建都跑30秒,加上注解后直接变成0秒。

最后说一个实用技巧:如果你想强制某个Task重新执行,可以用--rerun-tasks参数。比如gradle build --rerun-tasks。不过一般情况下别用,除非你在调试。

好了,Task的核心内容就这些。从创建到配置,从依赖到增量构建,每个点都值得你在实际项目中反复体会。记住一句话:好的Task设计,能让你的构建又快又稳