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的完整工作流程:
从这张图可以看得很清楚:Task提交任务到WorkerExecutor,WorkerExecutor把任务分发给多个Worker并行执行,最后统一收集结果。整个过程对开发者来说,只需要关心submit()和await()这两个操作。
实际项目中的最佳实践
说了这么多,最后总结几条我在实战中沉淀下来的经验:
- 任务粒度要适中:每个Worker处理的任务不要太碎。我见过有人把每个文件都提交一个Worker,结果创建Worker的开销比处理文件本身还大。一般来说,每个Worker处理50-100个文件比较合适。
- 注意内存控制:每个Worker进程都会占用独立内存。如果你的任务需要加载大文件,记得在
gradle.properties里调大org.gradle.jvmargs。 - 善用缓存:Worker API配合Gradle的构建缓存效果非常好。只要输入没变,Worker就不会重新执行。
- 调试时关掉并行:在开发调试阶段,建议把
max-workers设为1。并行执行时错误日志会混在一起,排查问题很痛苦。
最后说一句:Worker API是Gradle构建加速的利器,但它不是银弹。如果你的任务本身就不耗时,强行并行反而会增加复杂度。我建议你先用Gradle的--profile参数分析构建耗时,找到真正的瓶颈,再决定要不要上Worker。