探究 Android Studio 3.5 新增的 Apply Changes 功能。

Instant Run

  • 构建过程动手脚
  • Apk 里藏私货
  • 自定义 ClassLoader 热部署
  • 兼容性一般

Instant Run

Apply Changes

  • 不关心构建过程,只对 apk 做 diff
  • 基于 JVMTI,需要 Android O+

基本流程

  1. 对设备上安装的 apk 与新 apk 做 diff
  2. 将 diff 推到设备上并应用改动

改动类型支持

  1. 仅 res/assets 改动:应用改动的资源,重启 Activity
  2. 仅 dex 改动:改动不能影响对象的内存布局,基于 JVMTI 进行类级别的 hot swap
  3. 资源改动 + dex 改动
  4. 其他改动:不支持

增量更新流程

  1. 解析新 apk 的 zip entries
  2. 拉取已安装的 apk 的 zip entries
  3. 对以上两个列表做 diff,得出变更文件的列表
  4. 根据文件列表 diff 验证是否支持 apply changes(是否改动 dex,资源之外的内容)
  5. 根据 diff 计算(粒度为 zip entry 级别)dirty map,生成 instructions & patch。注意这里不需要全量拉取已安装的 apk
patchold apkdirtydirtypatch
  1. deltaPreInstall:adb 调用 cmd package install-create 创建 session,install-write 写入 instructions & patch 数据。
  2. 对 dex (里的类)进行 diff,得到需要 swap 的类列表。这里注意两点:
    (1)类的 diff 只用比较 checksum
    (2)dex 里类的信息其实是从数据库取的(预先写进去的)而非拉取设备上的 dex, ( >﹏<。)~ 这个问题卡了我好久,其实流程图上有写。
  3. Swap!adb 调用 installer 执行 swap
  4. Commit install

flow

zip

Swap & JVMTI Agent

  1. installer 进程开启一个 socket server
  2. 通过 cmd activity attach-agent <PROCESS> <FILE> attach agent 到指定进程
  3. Attach 成功后,agent 通过 socket 从 installer 读取数据
  4. InstrumentApplication:
    1. SetEventCallbackscallbacks.ClassFileLoadHook = Agent_ClassFileLoadHook
    2. 加载 instrumentation jar 包
    3. RetransformClasses,触发前面设置的 ClassFileLoadHook:Hook ActivityThread.handleDispatchPackageBroadcast(),更新资源。
  5. HotSwap:RedefineClasses

代码结构

1
2
3
4
5
6
7
8
agent/      JVMTI agent 实现,编译成 so 文件,拷贝到 app 的 data 目录下,通过 JVMTI attach 到对应的进程上。
bench.sh
common/
deployer/ Android Studio 端操作流程逻辑
installer/ 编译成二进制可执行文件,安装 apk 时会被 push 到 /data/local/tmp/.studio/bin/installer,跑在设备端,由 Android Studio 通过 adb 调用。目前支持三个操作:`dump`,`swap`,`deltapreinstall`。
proto/ PB 定义
summary.py
test/

核心逻辑可以参考 deployer/src/main/java/com/android/tools/deployer/Deployer.javaswap 方法,十分清晰。

1
2
3
4
5
6
7
8
9
// Get the list of files from the local apks
// Get the list of files from the installed app
// Calculate the difference between them
// Push the apks to device and get the remote paths
// Verify the changes are swappable and get only the dexes that we can change
// Compare the local vs remote dex files.
// Do the swap
// Update the database with the entire new apk.
// Wait only for swap to finish