CI分层,说白了就是把构建流水线拆成三段

很多团队在做 CI 的时候,喜欢把所有事情都塞到一个任务里。嗯,我早期也这么干过——单元测试、lint 检查、打包、签名、上传,全在一个 Gradle Task 里串起来。结果呢?改一行注释,整个流水线跑 20 分钟。后来我慢慢意识到,对于 AGP 升级这种大动作,CI 必须分层,每一层只干一件事,干完就停。

这其实不是什么高深理论。你想想看,一辆汽车从零件到下线,也是分冲压、焊接、涂装、总装几个车间的。CI 分层就是给 Android 构建流程划分车间。每一层有独立的职责、独立的触发条件、独立的失败处理策略。

我习惯把 CI 分成三层

这三层分别是:提交层(Commit Layer)、集成层(Integration Layer)、发布层(Release Layer)。每一层负责不同的质量关卡,越往上层,构建成本越高、触发频率越低。

核心原则:把昂贵的操作往后推,把快速反馈的操作放在最前面。AGP 升级后,很多插件和配置变了,分层逻辑要重新对齐,不然就会出现「打包过了但 lint 挂了」这种哭笑不得的场面。

下面这张图是我自己项目里常用的分层结构,你可以参考一下:

CI 流水线分层架构 提交层 (Commit Layer) 触发条件:每次 push 到任意分支 代码风格检查 单元测试 增量编译验证 (ktlint / detekt) (JUnit / Robolectric) (./gradlew assembleDebug) 集成层 (Integration Layer) 触发条件:PR 合并到 main/release 分支 UI 自动化测试 多模块打包验证 AGP 升级兼容性检查 (Espresso / Compose Test) (assembleRelease + R8) (API 差异 / 插件冲突) 发布层 (Release Layer) 触发条件:打 Tag / 手动触发 正式签名打包 构建产物归档 发布到分发平台 (jks / apksigner) (Apk / AAB / mapping) (Firebase / 蒲公英) 💡 关键点:AGP 升级后,集成层的「插件兼容性检查」是新增环节,建议用 ./gradlew build --refresh-dependencies 做全量验证。

每一层具体干什么?我拆给你看

咱们一层一层来看。先从最频繁、最便宜的提交层开始。

第一层:提交层(Commit Layer)—— 快准狠

这一层的核心诉求是 「快」。每次开发者 push 代码,5 分钟之内必须出结果。我通常只放三类检查:

  • 代码风格检查:ktlint、detekt、Checkstyle。别让 Reviewer 花时间在缩进问题上。
  • 单元测试:JUnit + Mockito / MockK,跑本地测试,不依赖模拟器。
  • 增量编译验证:跑一遍 ./gradlew assembleDebug,确保代码能编译通过。

为什么强调「增量编译」?因为全量编译太慢了。AGP 8.x 之后,增量编译的稳定性比以前好了很多,但还是会偶尔出现「增量编译过、全量编译挂」的情况。不过没关系,那是集成层该管的事。

一个小建议:提交层最好用 Git Hooks + CI 双保险。本地 pre-commit hook 跑 ktlint,CI 上再跑一遍。我见过团队只靠本地钩子,结果有人跳过了,代码里全是风格问题。

第二层:集成层(Integration Layer)—— 把模块拼起来看

这一层的触发条件通常是 PR 合并到主分支 或者 release 分支。跑的内容要比提交层重得多:

  • UI 自动化测试:Espresso、Compose Test、或者 Appium。必须上模拟器或真机。
  • 多模块打包验证:跑 ./gradlew assembleRelease,开启 R8 混淆。这一步特别吃内存,CI Runner 最好给 8GB 以上。
  • AGP 升级兼容性检查:这是新增的。检查各个 Gradle Plugin 版本是否兼容,API 调用是否有废弃。我会在 CI 里加一个 ./gradlew buildEnvironment 输出依赖树,对比升级前后的差异。

我在项目中遇到过一个问题:升级到 AGP 8.2 后,某第三方插件用了内部 API,直接编译失败。提交层没事,集成层一跑就挂了。后来我在集成层加了一步叫 「预编译检查」——用 --refresh-dependencies 配合 --no-build-cache 做一次干净的编译验证。这个习惯帮我挡了不少坑。

注意:集成层的 assembleRelease 如果开启 R8,构建时间会翻倍。建议和单元测试并行运行,不要串行。我曾经见过 CI 总时长 40 分钟,其中 R8 占了 25 分钟。

第三层:发布层(Release Layer)—— 只做最核心的事

发布层的触发频率最低,可能一周一次,甚至一个月一次。它的核心就是 「确定性」:每一次发布,产出的 Apk / AAB 必须一模一样(代码不变的前提下)。

  • 正式签名打包:用 jks 签名,走 apksigner 对齐。
  • 构建产物归档:Apk / AAB / mapping 文件 / 资源 R 文件,全部归档到制品库。
  • 发布到分发平台:上传 Firebase App Distribution、蒲公英、Google Play Console,或者内部 CDN。

这一层我特别强调 「人工确认」。AGP 升级后,构建产物可能有细微差异,比如资源 Shrinking 策略变了,或者 ProGuard 规则有了新行为。所以发布层建议加一个 手动审批门禁——CI 自动跑完打包和签名,然后由技术负责人确认构建产物对比无误,再点「发布」。

一个实用技巧:发布层的 Gradle 配置建议单独写一个 release.gradle 文件,通过 apply from: 'release.gradle' 加载。这样升级 AGP 时,只需要改这个文件里的配置,不会影响到其他层。

这三层怎么配合?看一个实际的例子

假设你的项目从 AGP 7.4 升级到 8.3。你以为升级完了就没事了?错了。升级后兼容性问题会冒出来。这时候分层的作用就体现出来了:

  1. 提交层:开发者 push 代码,CI 跑增量编译。如果 AGP 8.3 的增量编译有 bug,这里会先暴露。
  2. 集成层:PR 合并后,跑全量 assembleRelease + R8。这里可能暴露混淆规则不兼容、插件 API 废弃等问题。
  3. 发布层:打正式包时,对比 AGP 7.4 和 8.3 的构建产物差异,确认资源、字节码、签名无异常。

每一层都有独立的告警和回滚策略。提交层挂了,开发者自己修复;集成层挂了,PR 回退;发布层挂了,整个版本回滚。

表格总结:三层对比

维度 提交层 集成层 发布层
触发频率 每次 push PR 合并 打 Tag / 手动
目标时长 ≤ 5 分钟 ≤ 20 分钟 ≤ 30 分钟
核心检查 lint + 单元测试 + 增量编译 UI 测试 + 全量打包 + 插件兼容 签名 + 归档 + 分发
AGP 升级适配 增量编译验证 全量编译 + R8 兼容性 产物对比 + 签名确认

说到底,CI 分层不是什么新概念,但它能让你的构建流水线在面对 AGP 升级这种大变动时,更加可控。每一层有自己的边界,出了问题能快速定位,修复成本也低——不比你在一个 500 行的 shell 脚本里找 Bug 舒服多了?