Gradle Worker API:让构建任务真正并行起来

说实话,我刚接触Gradle Worker API的时候,第一反应是——这不就是个线程池吗?后来在项目里踩了坑才明白,它远不止这么简单。Worker API是Gradle提供的一套并行任务处理机制,专门用来解决构建过程中的性能瓶颈问题。

你想想看,我们平时写的Gradle任务,默认是在同一个进程里串行执行的。如果你的项目有几十个模块,每个模块都要做资源处理、代码生成、字节码转换……那构建速度简直让人抓狂。Worker API就是为了解决这个问题而生的。

Worker API的核心作用

Worker API说白了就是让你能把任务拆分成多个独立的工作单元,然后并行执行。它有几个关键特性:

  • 隔离性:每个工作单元在独立的Worker进程中运行,互不干扰
  • 并行性:自动利用多核CPU,最大化构建速度
  • 资源控制:可以限制最大并行数,避免把机器跑死
  • 结果收集:所有Worker执行完毕后,统一收集结果

重要概念:Worker API不是简单的线程池。它会在独立的进程里执行任务,这意味着每个Worker都有自己的类加载器、内存空间。这样做的好处是——不会出现类冲突、静态变量污染这些烦人的问题。

自定义WorkAction:从零开始写一个Worker

我记得第一次写WorkAction的时候,被它的接口设计惊艳到了。它不像传统的Task那样需要继承一大堆东西,而是通过接口+注解的方式,非常干净。

来看一个实际的例子。假设我们要对APK里的资源文件做并行压缩:

// 1. 定义WorkAction接口
abstract class CompressWorkAction : WorkAction<CompressParameters> {
    
    // 注入参数
    abstract override fun getParameters(): CompressParameters
    
    override fun execute() {
        val inputFile = parameters.inputFile.get()
        val outputFile = parameters.outputFile.get()
        val quality = parameters.quality.get()
        
        // 执行压缩逻辑
        compressImage(inputFile, outputFile, quality)
    }
}

// 2. 定义参数接口
interface CompressParameters : WorkParameters {
    val inputFile: Property<RegularFile>
    val outputFile: Property<RegularFile>
    val quality: Property<Int>
}

这里有个细节要注意——WorkAction是抽象类,不是接口。为什么?因为Gradle需要帮你注入参数。你只需要声明抽象的getParameters()方法,Gradle会自动实现它。

个人经验:参数接口里尽量用Property<T>而不是直接传值。这样Gradle可以帮你做延迟计算和缓存。我曾经图省事直接传String,结果每次构建都重新执行,白白浪费了增量构建的能力。

并行任务处理:让Worker跑起来

定义好WorkAction之后,怎么让它并行执行呢?我们需要在Task里使用WorkerExecutor

abstract class CompressTask : DefaultTask() {
    
    @get:Inject
    abstract val workerExecutor: WorkerExecutor
    
    @TaskAction
    fun compress() {
        // 获取所有需要压缩的文件
        val files = getInputFiles()
        
        // 提交并行任务
        val futures = files.map { file ->
            workerExecutor.submit(CompressWorkAction::class.java) {
                inputFile.set(file)
                outputFile.set(getOutputFile(file))
                quality.set(80)
            }
        }
        
        // 等待所有任务完成
        workerExecutor.await()
    }
}

嗯,这里要注意await()的调用位置。我见过有人把await()写在循环里,结果变成了串行执行——每次提交一个任务就等它完成,那还并行个啥?

Worker与Transform结合使用

说到Transform,这可是Android构建里的重头戏。传统的Transform是串行处理所有文件的,遇到大项目简直要命。把Worker API和Transform结合起来,就能实现真正的并行字节码处理。

我曾经在一个日活千万级的App项目里做过优化。那个项目的Transform要处理3000多个class文件,做混淆、插桩、日志收集。用传统方式要跑4分多钟,改成Worker并行后,直接压缩到50秒。

来看一个结合使用的示例:

class ParallelTransform : Transform() {
    
    override fun transform(context: TransformInvocation) {
        val workerExecutor = context.getWorkerExecutor()
        
        // 收集所有输入文件
        val inputs = context.inputs.toList()
        
        // 并行处理每个jar/文件夹
        val futures = inputs.map { input ->
            workerExecutor.submit(TransformWorkAction::class.java) {
                inputJar.set(input.file)
                outputDir.set(context.outputProvider.getContentLocation(
                    input.name,
                    input.contentTypes,
                    input.scopes,
                    Format.JAR
                ))
            }
        }
        
        // 等待所有Transform完成
        workerExecutor.await()
    }
}

避坑指南:我曾经犯过一个错误——在Transform里直接修改了输入文件。Worker是并行执行的,多个Worker同时写同一个文件,后果就是构建结果随机出错。记住:每个Worker只能写自己的输出文件,不要碰别人的地盘。

Worker API的配置与调优

Worker API虽然好用,但也不是无脑开并行就完事了。你需要根据机器配置来调整:

配置项 说明 建议值
max-workers 最大并行Worker数 CPU核心数 - 1
fork-mode Worker进程模式 默认即可
isolation-mode 隔离级别 CLASSLOADER

我个人习惯在gradle.properties里设置:

org.gradle.workers.max=4
org.gradle.parallel=true

为什么是CPU核心数减1?因为要留一个核心给系统和其他进程用。如果你有8核CPU,开8个Worker,机器可能会卡到鼠标都动不了。

知识体系总览

下面这张图展示了Worker API的完整工作流程:

Worker API 工作流程 Gradle Task WorkerExecutor.submit() Worker 1 处理文件组A Worker 2 处理文件组B Worker 3 处理文件组C await() 收集结果

从这张图可以看得很清楚:Task提交任务到WorkerExecutor,WorkerExecutor把任务分发给多个Worker并行执行,最后统一收集结果。整个过程对开发者来说,只需要关心submit()await()这两个操作。

实际项目中的最佳实践

说了这么多,最后总结几条我在实战中沉淀下来的经验:

  1. 任务粒度要适中:每个Worker处理的任务不要太碎。我见过有人把每个文件都提交一个Worker,结果创建Worker的开销比处理文件本身还大。一般来说,每个Worker处理50-100个文件比较合适。
  2. 注意内存控制:每个Worker进程都会占用独立内存。如果你的任务需要加载大文件,记得在gradle.properties里调大org.gradle.jvmargs
  3. 善用缓存:Worker API配合Gradle的构建缓存效果非常好。只要输入没变,Worker就不会重新执行。
  4. 调试时关掉并行:在开发调试阶段,建议把max-workers设为1。并行执行时错误日志会混在一起,排查问题很痛苦。

最后说一句:Worker API是Gradle构建加速的利器,但它不是银弹。如果你的任务本身就不耗时,强行并行反而会增加复杂度。我建议你先用Gradle的--profile参数分析构建耗时,找到真正的瓶颈,再决定要不要上Worker。

公众号:蓝海资料掘金营,微信deep3321