编译调度流程
常规 Android Run 会由 Gradle 完成工程配置、源码和资源编译、APK 构建等工作。Jugg 已有可信构建基线时,可以只处理本轮变化,但这要求编译结果、设备上的 APK 和部署历史始终对应同一轮成功状态。
因此,一次 Run 不是简单地在“增量编译”和“Gradle 编译”之间二选一。Jugg 还要判断当前现场是否允许增量,持续补编译受影响的文件,并在部署成功后才确认本轮结果。这页解释这条完整调度流程;Java、Kotlin、资源各自如何编译,见增量编译。
一次 Run 的编译调度全貌
开始 Run
-> 等待初始化和文件变化处理完成
-> 检查工程信息、构建目标、设备状态和变化规模
-> 不满足增量条件:执行 Gradle 或返回当前限制
-> 满足增量条件:编译当前变化文件
-> 产物写入 staging
-> 发现受影响源码或需要重新转换的 class:继续下一轮
-> 编译失败且能够改变失败条件:重试一次
-> 编译成功且 Git 补检发现遗漏文件:再执行一次增量编译
-> 使用 staging 产物部署
-> 所有目标设备部署成功:提交 staging 和部署历史
-> 部署失败:不提交,并按失败结果结束或整轮回退 Gradle这条流程有两个关键边界:编译成功不等于本轮已经提交;再次编译也不一定是在重试失败。影响扩散、失败重试和 Git 补检解决的是三类不同问题。
进入增量前先排除不可信现场
Jugg 会先等待初始化和待处理的文件事件完成,再用一组可短路的检查判断本轮是否适合增量。任何一条命中,都不会继续执行后续增量编译。
| 检查结果 | 本轮处理 |
|---|---|
| 用户强制全量 | 执行 Gradle。 |
| 缺少可用的工程信息 | 执行 Gradle,重新建立工程信息和构建基线。 |
| 构建目标在 app 与 androidTest 之间变化 | 执行 Gradle,建立对应 APK 的基线。 |
| 没有需要处理的文件变化 | 结束编译阶段,不制造空的增量产物。 |
| 目标设备或部署状态不支持增量 | 返回对应限制;需要重建时转入 Gradle。 |
| 改动文件或涉及模块过多 | 放弃本轮增量,避免局部编译范围继续扩大。 |
| build 文件发生变化 | 先刷新工程信息;只有依赖变化仍可由增量链路处理时才继续,否则执行 Gradle。 |
如果 build 文件没有变化,本轮刷新得到的就是当前最新工程信息,可以继续用于这次增量判定。build 文件发生变化时,刷新仍会提供最新工程信息,但它不能证明已有 APK 基线仍然有效,因此调度层还要根据依赖变化结果决定能否继续增量。详细规则见工程信息刷新。
判定阶段还会对照部署历史过滤文件:文件如果被改回设备上已经部署过的内容,就不再进入本轮变化集合,避免重复编译和部署。
编译产物先进入 staging
进入增量后,各编译链路不会立刻改写已部署状态,而是把本轮产物写入 staging 暂存区。它在一次 Run 中经历三个时间点:
- 每轮编译完成后,新产物加入 staging。
- 所有补编译结束后,部署阶段消费 staging 中的有效产物。
- 所有目标设备部署成功后,才提交 staging 和部署历史。
多 APK 工程还要在这个阶段保留产物归属。资源、Manifest、assets 等产物会携带它影响的 APK 集合;同一模块属于多个 APK 时,对应产物也必须进入多个目标的部署集合。归属错误会导致产物漏发或发往错误 APK。
成功后为什么还会继续编译
只编译直接改动的文件可能留下旧调用方。比如删除方法或修改字段类型时:
A 删除方法或修改字段类型
-> 未修改的 B 没有参与首轮编译
-> A 的局部编译成功
-> 旧 B 仍引用原来的成员
-> 部署后可能出现 NoSuchMethodError 或 NoSuchFieldError为避免把这种结构不一致带到设备上,每轮成功后,Jugg 会比较新旧 class 结构并查询引用关系,把受影响源码或需要重新转换的 class 加入下一轮。这个过程会持续到没有新影响、编译失败,或扩散范围已经不适合继续增量。
同一轮 Run 内,已经处理过的影响不会重复加入;由扩散产生的文件也不会被误记为用户最初修改的文件。机制细节见重编译 / 扩散编译。
三种再次编译解决不同问题
| 机制 | 触发条件 | 次数和结果 |
|---|---|---|
| 影响扩散 | 当前轮成功,但发现新的受影响源码或需要重新转换的 class | 可以继续多轮,直到影响收敛、失败或超出增量范围。 |
| 失败重试 | 当前轮失败,并且刷新文件变化或编译上下文能够改变失败条件 | 最多重试一次;仍失败时返回失败或回退 Gradle。 |
| Git 补检 | 增量编译期间完成的异步补检发现新的未编译文件 | 当前增量成功后再执行一次增量编译;没有新文件则不重复执行。 |
失败重试只处理已知且可恢复的情况。例如,符号缺失可能来自工程关闭期间或切换分支后遗漏的文件变化,此时刷新 Git 变化能够补齐输入;依赖类缺失时,刷新编译上下文可能补齐 classpath。重试必须改变失败条件,不会原样反复执行。
并非所有失败都会先重试。无法识别或无法局部恢复的异常可以直接进入回退判定;可恢复失败重试后仍未解决,也会根据失败结果返回当前错误,或执行 Gradle 重建基线。
部署成功后才提交本轮结果
增量编译完成后,部署阶段使用 staging 中的产物更新目标设备。只有所有目标设备都成功,本轮 staging、已部署文件状态和部署历史才一起提交。
如果部署失败,这些状态不会提交。否则下一次 Run 会误以为失败产物已经部署,并在错误基线上继续计算文件变化。部署失败后,调度层会按失败是否可由全量构建恢复,结束本轮并保留错误,或整轮转入 Gradle。
调度边界
- Jugg 增量依赖可信的 Gradle APK 基线。工程配置、依赖、注解处理器或生成代码的变化无法由本轮增量结果确认时,需要执行 Gradle。
- 影响扩散不是无限补编译。范围过大或中途失败时,会停止增量并进入对应的失败或回退处理。
- 单次编译成功只表示产物可以进入 staging,不表示设备状态和部署历史已经更新。
- 完整的 Gradle 回退条件和基线更新规则见Gradle 回退与基线重建。