Apply Changes 中的 class 与 overlay
Android Studio 的 Apply Changes 不会重新安装一份完整 APK。它以设备上已经安装的 APK 为起点,把本轮 class 变化和资源文件组织成一次增量更新,再决定是否重建当前 Activity。Jugg 复用这套设备端应用机制,但 class 和 overlay 的输入来自自己的增量编译结果。
理解这条路径可以解释两个常见现象:只修改方法体时为什么不用重装 App,以及 Run 结果显示 Hot Reload 时为什么当前 Activity 仍会重新执行生命周期。
Apply Changes 使用 APK 基线和局部更新
完整安装会把 APK 中的 DEX、资源和 Manifest 一起替换。Apply Changes 则保留已安装 APK,只发送相对当前部署状态发生变化的内容。
已安装 APK 与 deployment cache
-> 本轮 class 变化
-> 本轮资源和 assets overlay
-> 生成一次 overlay update
-> 写入设备并更新 overlay ID
-> 按部署类型保持进程、重建 Activity 或重启 Appdeployment cache 记录了设备上一次成功安装或 Apply Changes 后的 APK 快照和 overlay ID。新的局部更新必须基于这份快照生成;即使本轮没有 class 或资源需要下发,Apply Changes 仍会走完 overlay ID 校验和 checkpoint 更新。空 payload 不触发 class 处理、Activity 重建或 App 重启。如果本地记录和设备状态对不上,Jugg 会先进入 Recover,而不是继续叠加差异。
class 分为在线修改和新增内容
Apply Changes 对已加载 class 的在线修改依赖 JVMTI。Apply Changes Agent 取得 JVMTI 后,对 modified class 执行 class redefinition,因此方法体修改可以在不重启 App 进程的情况下生效。新增 class 不走 redefine;设备端把本轮新 DEX 转为 in-memory dex elements,追加到 Application ClassLoader 的既有 elements 后面,使当前进程能够加载新 class,同时 overlay 为后续进程启动保留同一份内容。字段、方法签名或继承关系等结构变化不能沿用这两条在线路径,需要转为 Hot Fix 并在 App 重启后加载。
run-as 可完整执行、UID 位于 10000..19999,且新建文件与 App 既有缓存目录使用相同 SELinux label 的 App 直接复用 Android Studio 通道;前提不成立、但普通 shell、root adbd 或非交互 su 能完整访问 sandbox 时,Jugg Direct sandbox 先持久化同一份 overlay,再按 Apply Changes 的 new class 与 modified class 处理方式更新当前进程。详情见 Jugg JVMTI Agent。
Jugg 在部署前比较新旧 class 结构,并把 class 变化交给 Apply Changes 的两个输入集合。
| class 变化 | Apply Changes 输入 | 生效边界 |
|---|---|---|
| 方法体变化,class 结构保持不变 | modified class | 由 JVMTI 在线替换已加载的 class 实现 |
| 新增 class | new class | 作为 in-memory dex elements 加入当前 Application ClassLoader,同时写入 overlay 供下一次进程加载 |
| 字段、方法签名、继承或泛型结构变化 | new class / Hot Fix 数据 | 不能依赖当前进程中的 class redefine,需要重启 App 后加载 |
| library dex、multi-dex 等不能稳定在线替换的 class | Hot Fix 数据 | 重启 App 后由运行时加载 |
方法体变化能够进入 modified class,不代表 Jugg 会让当前 Activity 保持不动。class 是否可以在线替换和界面是否需要刷新是两个不同判断:前者决定字节码怎样应用,后者决定用户何时能看到新的代码和资源结果。
overlay 承载资源、assets 和 DEX 文件
资源增量编译会输出 resources.arsc、res/** 或 assets/** 等局部文件,新增 class 和需要在后续进程启动时加载的 DEX 也会进入设备 overlay。Apply Changes 按目标 APK 组织这些文件,让 base APK、split APK 和 test APK 的内容写入各自的 overlay 位置。
首次向某个部署基线发送资源 overlay 时,Jugg 会补齐完整资源集合。设备端此前没有可复用的资源 overlay,只发送单个变化文件无法构成完整的新资源视图。后续部署已经有可信资源状态时,才继续叠加本轮差异。
Manifest 和配套资源表需要写回安装包,进入APK 更新与安装路径。native lib 在 SO hot update 开启时进入 overlay,并在重启 App 后加载;关闭时才进入 APK 更新路径。详见 so 更新。
Code Swap 和 Full Swap 的区别
Android Studio 的增量部署可以选择只应用变化,也可以在应用变化后重建 Activity。
| Apply Changes 动作 | class 与 overlay 处理 | 生命周期结果 |
|---|---|---|
| Apply Changes | 写入局部更新,可按需执行 class redefine | 不主动重建 Activity |
| Apply Changes and Restart Activity | 写入同类局部更新,并在完成后重建 Activity | App 进程保留,当前 Activity 重新执行生命周期 |
Jugg 当前对普通、非空且不要求重启 App 的增量部署使用 Apply Changes and Restart Activity。这样资源、布局以及在 Activity 生命周期中读取的新代码能够在当前界面重新加载。因此 Jugg 的 HOT_RELOAD 表示本轮仍属于在线增量部署,不表示 Activity 一定保持不变。
warm-up、状态探测或不需要界面刷新的特殊调用可以使用不重建 Activity 的 Apply Changes。需要 Hot Fix、兼容部署或进程级缓存刷新的产物则会重启整个 App,不由 Activity 重建收口。
哪些内容必须重启 App
以下变化不能只依赖 Activity 重建:
- class 结构变化或其它需要 Hot Fix 加载的 DEX;
- 兼容部署中只在重启后读取的 overlay;
- APK 根目录的 classpath resource;
- Compose 资源等具有进程级缓存的内容;
- 本轮部署中的 ARouter 注册索引;
- 用户开启“部署后始终重启”,或从 Debug 入口运行。
这些场景仍可使用本轮增量编译产物,但新进程需要重新建立 ClassLoader、Resources 或运行时缓存。具体加载方式见App 进程内 Jugg runtime和兼容部署。
ARouter 会把生成索引中的注册信息加载到进程级缓存。在线替换索引的方法体、重建 Activity,都不会重新建立已经加载的路由映射。因此 Jugg 在部署这些索引后重启 App,即使索引的 class 结构允许在线替换。触发范围与保守重启边界见 Restart。