Skip to content

重编译 / 扩散编译

一次源码修改可能删除方法、改变字段类型,或给抽象父类和接口新增抽象方法。直接改动的文件可以编译成功,旧 APK 中没有参与本轮编译的调用方或子类却仍按原来的方法签名、字段类型或抽象方法集合运行。如果只编译直接变更的源码,应用运行到相关代码时会抛出 NoSuchMethodErrorNoSuchFieldErrorAbstractMethodError

Jugg 会在首轮编译后继续分析影响范围,把需要适配新结构的源码加入下一轮。下文把这个过程称为影响传播,也就是日志和其他页面中的重编译或扩散编译。

直接改动编译成功,旧调用方仍可能失效

假设类 A 删除了一个方法,类 B 仍调用这个方法,但本轮只修改并编译了 A:

text
A 删除方法
  -> 未修改的 B 没有参与编译
  -> A 的首轮编译成功
  -> 设备上仍保留按旧签名编译的 B
  -> B 运行到该调用时抛出 NoSuchMethodError

给抽象父类或接口新增抽象方法也有相同风险。定义方可以通过编译,旧子类或实现类却没有实现新增方法,运行时可能抛出 AbstractMethodError

因此,首轮没有编译错误只能说明直接输入有效,不能证明旧 APK 中的调用方已经适配本轮结构变化。

为什么不直接重编整个模块

只编直接改动的文件会漏掉旧调用方;每次重编整个模块又会把大量无关源码带回编译链。Jugg 对比新旧 class,只把方法、字段、新增抽象方法和类级泛型 signature 等已确认差异转换成传播信号,再从历史引用关系中找出需要重新编译的源码。

查询基于最近一次可信的 Gradle 构建产物和后续成功部署的产物。索引记录方法调用、字段访问和继承关系,让 Jugg 能从“什么发生了变化”反查“谁仍依赖旧结构”。

Jugg 如何找到受影响源码

首轮源码生成 DEX 后,Jugg 对比新 class 与基线中的旧 class,把结构变化转换成影响信号,再查询调用方和继承关系:

text
首轮源码编译成功
  -> 对比新旧 class 结构
  -> 从引用索引查找调用方、字段访问方和子类
  -> 把命中的 class 还原为源码
  -> 将这些源码加入下一轮编译

不同结构变化对应不同的传播方向:

结构变化需要检查的旧代码
方法删除、签名变化或影响调用方式的访问修饰变化调用该方法的源码;实例方法还要考虑继承关系。
字段删除,或类型变化导致旧字段签名消失访问旧字段的源码。
抽象父类或接口新增抽象方法子类和实现类。
类级泛型签名变化直接成员调用方和继承链中的子类。

只修改方法体不会产生上述影响信号,因此不会触发调用方重编译。这个判断只决定是否传播源码;本轮 class 最终通过在线替换、热修复还是重启生效,仍由部署条件决定。

为什么影响可能传播到下一轮

受影响源码重新编译后,也可能产生新的结构变化。例如 A 的修改让 B 必须重编,而 B 的新产物又改变了 C 依赖的结构。C 只能在分析 B 的新产物后被发现。

text
A 的结构变化
  -> B 被加入第二轮编译
  -> B 的新产物产生新的影响信号
  -> C 被加入下一轮编译
  -> 没有新的受影响源码时结束

Jugg 在每轮成功后重新计算影响范围,而不是假定首轮可以一次列出所有文件。同一传播来源会被去重;出现新的结构变化或新的影响来源时,才会继续下一轮。

传播如何避免失控

影响传播不会无条件沿引用关系展开:

  • 实例方法可能通过虚方法分发影响子类,静态方法只检查直接调用方,不沿子类树传播。这样可以避免 lambda 和编译器生成的静态方法把整棵继承树拉入编译。
  • R$... 资源类不参与方法和字段传播。资源修复会产生大量字段差异,把这些差异当作普通源码结构变化会导致大面积无效重编译。
  • 重复命中的传播来源会被过滤,避免相同文件在多轮之间来回编译。
  • 传播后的源码或模块超过增量编译范围时,Jugg 会停止继续扩大本轮工作,回退 Gradle 构建。

分析结果何时成为可信历史

影响分析得到的是本轮待处理结果,不是已经生效的状态。只有编译和部署全部成功后,新的 class 结构与引用历史才会提交,供下一次增量编译使用。

如果后续编译、部署或用户取消导致本轮没有完成,历史不会提前推进。下一次编译仍能基于原有可信状态重新发现这批影响,避免失败轮把索引推进到设备尚未生效的版本。

普通结构传播覆盖不到什么

影响传播依赖基线 APK 和已部署产物中的引用关系。切换构建变体、修改构建配置或其他情况让基线失去可信度时,需要通过 Gradle 构建重新建立索引。

泛型传播也有范围限制:它能覆盖子类声明链,以及 DEX 中存在直接方法或字段引用的调用方;只有源码约束、但 DEX 中没有直接成员引用的间接场景,不保证能够命中。

编译期常量和 release 混淆还有各自的影响来源:

  • const valstatic final 可能被直接内联为字面量,普通方法或字段引用索引看不到这些使用方,需由常量引用分析单独处理。
  • R8 / ProGuard 可能内联方法或裁剪成员,这些情况由 release 增量编译进行字节码补偿,不在普通源码传播中展开。

相关页面