重编译 / 扩散编译
一次源码修改可能删除方法、改变字段类型,或给抽象父类和接口新增抽象方法。直接改动的文件可以编译成功,旧 APK 中没有参与本轮编译的调用方或子类却仍按原来的方法签名、字段类型或抽象方法集合运行。如果只编译直接变更的源码,应用运行到相关代码时会抛出 NoSuchMethodError、NoSuchFieldError 或 AbstractMethodError。
Jugg 会在首轮编译后继续分析影响范围,把需要适配新结构的源码加入下一轮。下文把这个过程称为影响传播,也就是日志和其他页面中的重编译或扩散编译。
直接改动编译成功,旧调用方仍可能失效
假设类 A 删除了一个方法,类 B 仍调用这个方法,但本轮只修改并编译了 A:
A 删除方法
-> 未修改的 B 没有参与编译
-> A 的首轮编译成功
-> 设备上仍保留按旧签名编译的 B
-> B 运行到该调用时抛出 NoSuchMethodError给抽象父类或接口新增抽象方法也有相同风险。定义方可以通过编译,旧子类或实现类却没有实现新增方法,运行时可能抛出 AbstractMethodError。
因此,首轮没有编译错误只能说明直接输入有效,不能证明旧 APK 中的调用方已经适配本轮结构变化。
为什么不直接重编整个模块
只编直接改动的文件会漏掉旧调用方;每次重编整个模块又会把大量无关源码带回编译链。Jugg 对比新旧 class,只把方法、字段、新增抽象方法和类级泛型 signature 等已确认差异转换成传播信号,再从历史引用关系中找出需要重新编译的源码。
查询基于最近一次可信的 Gradle 构建产物和后续成功部署的产物。索引记录方法调用、字段访问和继承关系,让 Jugg 能从“什么发生了变化”反查“谁仍依赖旧结构”。
Jugg 如何找到受影响源码
首轮源码生成 DEX 后,Jugg 对比新 class 与基线中的旧 class,把结构变化转换成影响信号,再查询调用方和继承关系:
首轮源码编译成功
-> 对比新旧 class 结构
-> 从引用索引查找调用方、字段访问方和子类
-> 把命中的 class 还原为源码
-> 将这些源码加入下一轮编译不同结构变化对应不同的传播方向:
| 结构变化 | 需要检查的旧代码 |
|---|---|
| 方法删除、签名变化或影响调用方式的访问修饰变化 | 调用该方法的源码;实例方法还要考虑继承关系。 |
| 字段删除,或类型变化导致旧字段签名消失 | 访问旧字段的源码。 |
| 抽象父类或接口新增抽象方法 | 子类和实现类。 |
| 类级泛型签名变化 | 直接成员调用方和继承链中的子类。 |
只修改方法体不会产生上述影响信号,因此不会触发调用方重编译。这个判断只决定是否传播源码;本轮 class 最终通过在线替换、热修复还是重启生效,仍由部署条件决定。
为什么影响可能传播到下一轮
受影响源码重新编译后,也可能产生新的结构变化。例如 A 的修改让 B 必须重编,而 B 的新产物又改变了 C 依赖的结构。C 只能在分析 B 的新产物后被发现。
A 的结构变化
-> B 被加入第二轮编译
-> B 的新产物产生新的影响信号
-> C 被加入下一轮编译
-> 没有新的受影响源码时结束Jugg 在每轮成功后重新计算影响范围,而不是假定首轮可以一次列出所有文件。同一传播来源会被去重;出现新的结构变化或新的影响来源时,才会继续下一轮。
传播如何避免失控
影响传播不会无条件沿引用关系展开:
- 实例方法可能通过虚方法分发影响子类,静态方法只检查直接调用方,不沿子类树传播。这样可以避免 lambda 和编译器生成的静态方法把整棵继承树拉入编译。
R$...资源类不参与方法和字段传播。资源修复会产生大量字段差异,把这些差异当作普通源码结构变化会导致大面积无效重编译。- 重复命中的传播来源会被过滤,避免相同文件在多轮之间来回编译。
- 传播后的源码或模块超过增量编译范围时,Jugg 会停止继续扩大本轮工作,回退 Gradle 构建。
分析结果何时成为可信历史
影响分析得到的是本轮待处理结果,不是已经生效的状态。只有编译和部署全部成功后,新的 class 结构与引用历史才会提交,供下一次增量编译使用。
如果后续编译、部署或用户取消导致本轮没有完成,历史不会提前推进。下一次编译仍能基于原有可信状态重新发现这批影响,避免失败轮把索引推进到设备尚未生效的版本。
普通结构传播覆盖不到什么
影响传播依赖基线 APK 和已部署产物中的引用关系。切换构建变体、修改构建配置或其他情况让基线失去可信度时,需要通过 Gradle 构建重新建立索引。
泛型传播也有范围限制:它能覆盖子类声明链,以及 DEX 中存在直接方法或字段引用的调用方;只有源码约束、但 DEX 中没有直接成员引用的间接场景,不保证能够命中。
编译期常量和 release 混淆还有各自的影响来源:
const val和static final可能被直接内联为字面量,普通方法或字段引用索引看不到这些使用方,需由常量引用分析单独处理。- R8 / ProGuard 可能内联方法或裁剪成员,这些情况由 release 增量编译进行字节码补偿,不在普通源码传播中展开。