Skip to content

部署数据与影响分析

增量编译产出变化产物后,部署前还要回答两个问题:哪些产物下发给设备,哪些源码必须再编译一轮。这一步决定了部署结果是否与一次完整构建等价。

部署阶段需要同时处理两类事实:旧 APK 中仍然引用本轮变化的调用方,以及不同产物在设备上的生效路径。DEX、resource overlay、Manifest 和 native lib 的生效方式不同,Jugg 先做影响分析和产物归类,再决定继续补编译、在线替换、热修复或写回 APK。

只部署改动文件为什么不安全

最直觉的增量做法是“改了哪个 class 就只下发哪个 class”。但 class 之间存在引用关系:删掉一个方法、改掉一个字段签名,被改 class 自身能编译通过,可旧的调用方仍留在设备上的 APK 或已部署历史里,运行时继续按旧签名调用,于是抛出 NoSuchMethodErrorAbstractMethodError 这类问题。

编译期只看到“被直接修改的文件”,运行时崩溃却发生在“依赖这些文件的调用方”。增量部署要正确,就必须在下发前把受影响的调用方一并找回来重新编译。Jugg 把这一步拆成两件事:把产物按生效方式分组,以及沿引用关系做防御性扩散补编译。

把产物按生效方式分组

Jugg 不按“文件类型”分组,而按“在设备上如何生效”分组。每一组对应一条部署路径,避免用错误的方式应用产物。

产物分组包含什么生效方式
新增 class旧索引里不存在的 class随热修复或重启路径生效
可在线替换的 class新旧结构对比后判定结构未变的 class走 Apply Changes / JVMTI 在线替换
需重启生效的 class结构变化、跨 dex 或库 dex 归属更复杂的 class走热修复,重启后生效
资源与 assets overlay资源、assets 编译产物以 overlay 下发;首轮会补齐全量资源
写回 APK 的文件Manifest、配套 resources.arsc、native lib写进 APK、重签名后安装
受影响调用方影响分析找回的源码加入下一轮源码编译
常量引用命中源码常量引用分析单独命中的源码单独入口,加入下一轮源码编译

本轮走在线替换、热修复还是写回 APK,由这些分组反推:有写回 APK 的文件就先更新 APK,有需重启生效的 class 就本轮重启 App,其余可在线替换的部分尽量在线生效。

NOTE

资源首次以 overlay 下发时,会补齐一份全量资源,避免设备端 overlay 缺文件。所以不能只用“本轮改了几个资源”来判断设备端资源是否完整。

引用扩散:把受影响调用方找回来

影响分析的核心是新旧结构对比加引用查询。Jugg 先比较被改 class 的新旧结构,把变化压缩成几类信号,再用 APK 解析索引和增量部署索引查出引用方,最终落到需要重编译的源码文件。

text
变化 class
  -> 对比方法、字段、抽象方法和类级泛型签名
  -> 沿引用索引查调用方、字段访问方、子类
  -> 把受影响源码加入下一轮编译

会触发调用方重编译的结构信号包括:

  • 方法删除、方法签名变化、private 与非 private 之间切换,或其他会改变调用约定的访问标志变化。
  • 字段删除。
  • 抽象父类或接口新增抽象方法。
  • 类级泛型签名变化。

扩散有明确的传播规则:实例方法的变化会沿子类继续传播,因为虚方法分发可能落到子类;静态方法只查直接引用方,不启动子类遍历,否则编译器生成的静态合成方法会误触发整棵子类级联重编译。仅方法体内部变化(结构不变)不触发调用方重编译,因为它能安全在线替换。

release/minify 下的字节码补偿

release 构建经过 R8/ProGuard 的内联与裁剪,普通源码引用分析不足以覆盖两类隐患,需要额外补偿:

  • 被移除的成员:增量 DEX 引用了 APK 中已被裁剪掉的类或成员,需要补回。
  • 被内联的实现:某个被改方法曾经被内联进调用方,改了定义却没改内联副本,需要把持有旧副本的调用方一起补偿。

这两类补偿是字节码层面的修正。合并结果时有一条优先级:如果某个 class 已经被判定需要源码重编译,就保留源码重编译,不降级为字节码补偿。源码重编译覆盖面更大,反向不成立。

常量引用为什么单独走一条入口

常量引用是一个容易被漏掉的边界。const val 这类常量在编译期会被内联进所有使用方,改了定义方,使用方的字节码里却还是旧值。普通的类引用扩散追踪的是“运行时引用”,看不到这种“编译期已固化”的使用点。

因此常量引用有独立的分析入口,命中的源码单独加入下一轮编译,最后再和普通受影响源码合并。这样改一个常量不会只更新定义方而漏掉全部内联使用方。

影响分析的边界

这套分析依赖几个前提,也有清晰边界。它为了正确会把受影响调用方都拉进来,扩散范围越大,本轮要编译的文件就越多;当范围过大时,继续增量未必比直接 Gradle 更快。泛型扩散也不是无所不包:它只覆盖子类声明链和对变化类的直接引用方,DEX 擦除后那些没有直接成员引用的间接泛型约束变化,不保证一定命中。

历史索引也必须可信。引用查询基于上一次基线建立的 APK 与部署索引,一旦基线过期或被旁路改动,分析结果也会失真,这时需要回到 Gradle 重建基线。还要分清职责:产物分组只决定“怎么发”,不决定“一定成功”;能否在设备上真正生效,仍取决于设备状态和运行时兼容性,见部署策略部署状态与恢复

相关页面