编译调度流程
这页不讲 Java、Kotlin、资源各自怎么编译,那些内容在增量编译子页里。这里只讲调度层:一次 Run 如何决定走增量还是 Gradle,增量产物如何进入 staging,什么时候继续补编译,什么时候停下并回退。
调度层要同时避开两类问题:判定太重会吃掉增量收益,判定太松会把本应回退 Gradle 的场景误当成可增量。Jugg 用分层判定先排除不可信现场,再用 staging、扩散补编译和单次重试控制后续流程。
判定本身的开销与误判风险
增量编译只有在"判断现在能不能安全增量"这一步足够便宜时才划算。完整解析工程会把判定成本拉回 Gradle 级别,抵消本轮省下的编译时间。
更隐蔽的是误判风险。一次 Run 的安全性取决于多个互相独立的条件:用户是否主动要求全量、构建脚本或依赖是否变化、目标设备状态是否可信、本轮改动规模是否超出增量收益区间。任何一个条件被忽略,都会把一个本应回退 Gradle 的场景误判成"可增量",让设备拿到与全量构建不一致的产物,直到运行时才暴露问题。
分层判定:先排除不可增量,再进入增量
Jugg 把判定拆成一组从粗到细、可短路的检查。任何一条命中,就直接给出回退或失败结论,不再往下走;全部通过才进入增量编译。
| 判定 | 命中后的处理 |
|---|---|
| 用户强制全量 | 直接回退 Gradle。 |
| 构建目标(app / androidTest)变化 | 回退 Gradle,重建对应 APK 基线。 |
| 目标设备状态不可信 | 返回设备状态给出的失败原因,不进入增量。 |
| 改动文件或涉及模块过多 | 判定增量收益不足,返回"改动过多"。 |
| 构建脚本变化 | 读取依赖变化结果,再决定是否仍可走依赖库增量。 |
| 部署状态不支持增量 | 返回当前部署状态的限制原因。 |
判定阶段还会用部署历史做一次文件回滚过滤:如果某个文件被改回了已经部署过的内容,它会被移出本轮变化集合,避免无意义的重编译。
这套分层判定的设计目标是"廉价地证伪"。绝大多数日常 Run 在前几条就能确认可增量,无需触发更重的依赖解析或设备校验。
staging 推进:产物先暂存,成功才提交
进入增量后,本轮所有编译产物先写入 staging 暂存区,而不是直接更新全局基线和部署历史。部署阶段只消费 staging 中本轮有效的产物。
检测到的变化文件
-> 按类型进入各编译链路
-> 产物写入 staging
-> 首轮成功后查询是否还有受影响源码
-> 整轮部署成功后才提交为新的历史状态这里有一条关键状态边界:只有整轮部署成功后,staging 才提交为新的历史基线;失败轮不能更新全局历史。否则下一轮会以一个不完整的基线继续增量,造成累积性偏差。
成功后的扩散补编译
只编译直接改动的文件并不安全。改动文件本身可能编译通过,但删除方法、修改字段签名或给抽象父类新增抽象方法时,旧的调用方或子类会在运行时抛出 NoSuchMethodError、AbstractMethodError。
一轮成功后,调度层会基于新旧 class 结构对比和引用索引,查出还需要重新编译的源码,把它们当作新输入递归进入下一轮编译。两条去重约束负责让扩散收敛:
- 首轮成功的文件会从待编译集合移除;后续补编译轮不再更新这组状态,避免把派生出来的重编译文件误当成用户的原始改动。
- 同一轮 Run 内,对相同影响已经跟编过的源码不再重复跟编;只有出现新的触发来源时才继续下一轮。
扩散补编译的机制细节见重编译 / 扩散编译。
失败后的单次重试与回退
增量失败不会立刻回退 Gradle,而是先做一次有针对性的重试解析。重试只进行一次,按以下顺序尝试修复:
- 出现
unresolved reference、cannot find symbol这类符号缺失错误时,刷新 Git 变化,把工程关闭期间或切分支带来的遗漏文件补进本轮。 - 命中依赖缺失类错误时,刷新编译上下文后再编一次。
重试仍失败时,分两种收口:如果失败不可自动回退,返回当前增量失败结果,并提示下一次运行将走 Gradle;如果可以回退,则进入 Gradle 重建基线。
增量成功后还有一次异步 Git 补检。它只在补检发现"新的待编译文件"时才再触发一轮增量;仅仅是已编译文件的部署状态变化,不会重复触发。
多 APK 下的产物归属
多 APK 工程里,资源、Manifest、assets 这类产物必须带上它真正影响的 APK 集合。调度层按模块所属的 APK 分流编译,一个模块可以属于多个 APK。如果产物丢失了目标 APK 归属,部署阶段会把它发到错误的 APK 或漏发,所以这是分流环节的硬约束。
调度边界
- 增量失败的自动重试只有一次;一次重试无法恢复就进入回退判定,不会反复重试。
- 扩散范围越大,本轮要编译的文件越多;扩散范围超过增量收益区间时,分层判定会回退 Gradle。
- 调度依赖一个可信的 Gradle 基线。构建脚本、依赖、注解处理器或生成代码无法由本轮增量结果确认时,必须回到 Gradle 刷新基线(见回退与限制)。