如何在 Gradle 中定义自定义构建任务(Task)?
解读
国内 Android 面试中,Gradle 脚本能力被视为“工程化深度”的分水岭。面试官抛出此题,往往不是想听你背出“task hello {}”这种入门语法,而是考察三件事:
- 你能否把“自定义 Task”与真实 CI/CD 场景(如字节码插桩、渠道包二次签名、AAR 上传 Maven 仓库、产物自动归档飞书群通知)结合起来;
- 你能否区分“DSL 声明式写法”与“Task 类继承式写法”,并说出增量构建、缓存、并行对 Task 的约束;
- 你能否解释清 Gradle 生命周期(Initialization → Configuration → Execution)与 Task 依赖顺序,避免在 Configuration 阶段做 IO 或 Git 读取导致构建速度被拉长——国内大厂对“秒级构建”极度敏感,简历上写“优化构建 30%”却答不出生命周期,会被直接判负。
知识点
- 三种写法:DSL 声明、Task 容器注册、继承 DefaultTask
- 输入输出注解:@InputFiles、@OutputFile、@TaskAction,决定 UP-TO-DATE 与 Build Cache 命中率
- 生命周期钩子:afterEvaluate、gradle.projectsEvaluated、taskGraph.whenReady,避免 Configuration 阶段误执行
- 增量与并行:@PathSensitive、@IncrementalTaskInputs(Gradle 7.x 已迁移为 InputChanges)
- 国内特色:渠道包、R 文件常量内联、AAR 上传阿里云 Maven、企业微信/飞书构建结果机器人,均需自定义 Task 承载
- 性能红线:Configuration 阶段禁止 IO、网络、Git 命令;Execution 阶段才能做重活
- 缓存合规:自定义 Task 必须声明输出,否则 CI 第二次构建不会命中缓存,浪费云主机费用
答案
在 Android 工程中,自定义 Task 推荐采用“register 方式 + 继承 DefaultTask”组合,既享受 Configuration Cache,又方便单元测试。下面以“自动上传 APK 到阿里云 OSS,并发送飞书卡片消息”为例,给出可直接落地的样板代码,覆盖输入输出、增量缓存、并行与生命周期四个关键点。
步骤 1:在 buildSrc 模块(或 includeBuild 复合构建)中新建 UploadApkTask.kt
abstract class UploadApkTask : DefaultTask() {
@get:InputFile
@get:PathSensitive(PathSensitivity.NAME_ONLY)
abstract val apkFile: RegularFileProperty
@get:Input
abstract val ossBucket: Property<String>
@get:Input
abstract val ossObjectKey: Property<String>
@get:Internal // 飞书 webhook 不参与缓存
abstract val feishuHook: Property<String>
@TaskAction
fun upload() {
val file = apkFile.get().asFile
require(file.exists()) { "APK ${file.absolutePath} 不存在" }
// 1. 上传 OSS(省略 AK/SK 初始化)
val ossClient = OSSClient(/* ... */)
ossClient.putObject(ossBucket.get(), ossObjectKey.get(), file)
// 2. 构造飞书卡片
val payload = """
{
"msg_type": "post",
"content": {
"post": {
"zh_cn": {
"title": "构建完成",
"content": [
[{"tag": "text", "text": "包名:${file.name}"}],
[{"tag": "a", "text": "下载", "href": "https://${ossBucket.get()}.oss-cn-hangzhou.aliyuncs.com/${ossObjectKey.get()}"}]
]
}
}
}
}
""".trimIndent()
// 3. 发送飞书
val conn = URL(feishuHook.get()).openConnection() as HttpURLConnection
conn.requestMethod = "POST"
conn.doOutput = true
conn.setRequestProperty("Content-Type", "application/json")
conn.outputStream.write(payload.toByteArray())
check(conn.responseCode == 200) { "飞书通知失败:${conn.responseCode}" }
}
}
步骤 2:在 app 模块的 build.gradle.kts 中注册任务
android.applicationVariants.all {
val variant = this
val uploadTask = tasks.register<UploadApkTask>("upload${variant.name.capitalize()}Apk") {
apkFile.set(variant.packageApplicationProvider.flatMap { it.apkDirectory.file(variant.outputs.first().outputFileName) })
ossBucket.set("my-app-releases")
ossObjectKey.set("android/${variant.name}/${variant.versionName}/app-${variant.name}-${variant.versionName}.apk")
feishuHook.set(providers.gradleProperty("feishu.hook").orElse(""))
}
// 让 uploadTask 依赖于打包任务,确保 APK 已生成
uploadTask.dependsOn(variant.assembleProvider)
}
步骤 3:CI 调用
./gradlew assembleRelease uploadReleaseApk -Pfeishu.hook=https://open.feishu.cn/.../webhook/...
要点总结
- 使用 abstract class + Property<T> 写法,Gradle 可以生成实现类并支持 Configuration Cache,Android Studio 同步速度提升 20% 以上;
- @InputFile 与 @OutputFile 必须成对出现,否则 UP-TO-DATE 机制失效,每次 CI 都会重复上传;
- 飞书 webhook 属于副作用,标记为 @Internal,避免缓存命中时跳过通知;
- 所有 OSS 参数通过 Property 注入,方便在 CI 控制台动态替换,无需改脚本;
- 任务注册放在 afterEvaluate 之外,防止 Configuration 阶段触发文件 IO;
- 若需增量上传,可进一步监听 InputChanges,仅上传变更的 APK,适合大型 Monorepo。
拓展思考
- 国内多渠道场景下,Walle/VasDolly 生成上百个渠道包,如何写一个“批量上传并并发通知”的 Task,避免线程爆炸?(提示:使用 Worker API 把每个渠道包封装为 UnitOfWork,限制最大并行度为 CPU 核心数,防止 OSS 连接数被打满)
- 企业合规要求“构建产物必须秒级备份到私有云”,但备份 Task 耗时较长,如何让它与测试任务并行,又不阻塞 assemble?(提示:使用 finalizerTask,让备份任务成为 assemble 的“清理型”后继,Gradle 7.4+ 支持 finalizer 并行)
- 自定义 Task 需要读取 Git Commit 数量作为版本号,但 Configuration 阶段读取会导致 Configuration Cache 失效,如何优雅解决?(提示:使用 ValueSource<GitInfo, Parameters>,把 Git 命令延迟到 Execution 阶段,兼顾缓存与动态版本)
- 国内部分厂商 ROM 对 APK 签名算法有白名单,如何在 Task 链中插入“校验签名算法”步骤,失败直接中断构建?(提示:注册一个 VerifySignatureTask,依赖 packageApplicationProvider,校验 APK 的 SignatureScheme 版本,若小于 APK Signature Scheme v2 则抛出 GradleException,CI 直接红灯)
- 多人协作时,buildSrc 变更会触发全量编译,如何把自定义 Task 拆成独立 includedBuild,实现“二进制复用”与“版本化管理”?(提示:使用 composite build,把 UploadApkTask 发布到内部 Maven,版本号跟随 Git Tag,CI 自动拉取对应版本,避免本地反复编译 buildSrc)
掌握以上思路,面试时不仅能答出“怎么写 Task”,还能把“增量构建、并行、缓存、合规、CI 成本”讲透,直接对标国内一线大厂对 Gradle 工程化的最高要求。