Android Instant-Run 最新原理剖析
一、背景
1.参考:深度理解Android InstantRun原理以及源码分析
2.时间:2017.08.16
3.AS版本:2.3.3
4.TAG:InstantRun(开启此 TAG 可以看到 instant-run 的相关输出,帮助理解)
二、最新原理
上面那篇文章其实已经分析得比较到位了,但是随着 google 对 instant-run 的升级,文章里的一些东西已经发生了变化,比如不再使用 IncrementalClassLoader,也不再使用 BootstrapApplication 进行代理了。
1.运作流程
a.程序如何运行起来的?
在设置里勾选使能 instant-run 的情况下,运行程序,程序会在手机上正常跑起来,同时在 app/build/outputs/apk/ 目录下会生成一个名为 app-debug.apk 的文件。我们现在把 app 卸载掉,单独以 adb install -r app-debug.apk 的方式安装 app 到手机,点击桌面图标启动 app,会发现报 class not found 的错误!
我们解压 app-debug.apk 可以看到只有两个 dex,使用 d2j-dex2jar.sh 转换成 jar 文件再用 JD-GUI 查看,可以看到,这里只有 instant-run 相关的宿主代码,根本没有我们的业务代码。这也是驱动我去看 instant-run 代码的最原始的动力,奇了怪了,没有业务代码你是怎么跑起来的呢?
上面的参考文章里说了业务代码在 instant-run.zip 中,但是我们解压 app-debug.apk 也没有发现这个 zip 包啊!经过一番折腾,最后才发现,AS 使用了如下的安装命令:
adb install-multiple -r -p com.xxx.example.titan /xxx/Titan/app/build/intermediates/split-apk/debug/slices/slice_1.apk /xxx/Titan/app/build/outputs/apk/app-debug.apk
我勒个乖乖,难怪呢,原来使用了 install-multiple 命令同时安装了两个 apk,此时找到 slice_1.apk,解压确实看到了我们的业务代码。一样的,业务代码的每个方法前都插入了代码,参考上面给出的文章。
同时,root 过的手机,进入 /data/app/com.xxx.example.titan/ 目录下,也是可以发现我们的业务代码的,说明业务代码确实是安装到了手机上的。
b.程序和 as 如何通信的?
这里,建议看官们下载 google 官方的源码阅读,大致有如图所示的代码:
instant-run 源码目录主要通信的类是 InstantRunService 和 Server 两个类,现从 app-debug.apk 中解压出来的类来看,InstantRunService 已经不用了,改成了 InstantRunContentProvider,如图:
app-debug.apk 解压出来的 dex 代码/**
* Service which starts the Instant Run server; started by the IDE via
* adb shell am startservice pkg/service
*/
public classInstantRunServiceextendsService {
...
}
从官方的注释可以看出,通过 as 先启动 service(现在是 content provider),然后在 provider 中,创建了 Server 实例,并启动了 server,开启了 socket 等待 as 的连接。
然后就是协商协议版本,读取消息头,决定后续处理步骤等工作了。
2.源码之 Server#handleHotSwapPatch()
a.客户端(AS 端)
当改动了我们的业务代码时,比如 MainActivity.java 的 onCreate() 方法,然后点击 AS 上的闪电一样的符号,表示开始进行热部署,此时 AS 会生成一个 dex 文件,包含两个类,一个名为 MainActivty$override;另外一个叫 AppPatchesLoaderImpl,这个类继承 AbstractPatchesLoaderImpl,主要是要靠 AS 实现一个虚方法:
public abstract String[] getPatchedClasses();
另外还有一个由 AbstractPatchesLoaderImpl 实现了的方法:
public boolean load() {
...
}
b.服务端(app 端)
AS 通过 socket 告诉 app,我有代码改动了,需要进行部署。此时,app 通过 socket 得到这个消息,然后进入 handleHotSwapPatch() 方法进行处理。
首先,把 AS 生成的那个 dex 文件存入 /data/data/files/instant-run/ 目录下,然后构造一个 DexClassLoader,把这个 dex 作为寻找路径。
然后反射创造 AppPatchesLoaderImpl 实例,调用其 getPatchedClasses() 方法,获取到一个列表,这个列表表明了哪些代码发生了改变,即需要“修复”的类列表,比如这里,列表中只有一个数据,就是 MainActivity。
跟着,就进入了 AbstractPatchesLoaderImpl#load() 方法了,通过一个循环,读取所有列表,比如读到 MainActivity 时:
1.先通过反射创建一个 MainActivity$override 实例;
2.再用 ClassLoader 把 MainActivity load 进来,由于在最开始 gradle 编译 app-debug.apk 时,就使用 asm 等字节码操作工具给每个类都生成了一个 $change 静态成员,同时在每个方法的开头都插入了逻辑,判断 $change 是否为空,为空则走正常逻辑,否则走修复后的逻辑;这里通过反射直接把 MainActivity$override 实例赋值给了 $change 成员;同时,如果以前已经热部署过了一次或者多次,会把 $change 成员的 $change 字段置为 true,表明之前的过期了。
3.源码之 Server#restart()
由于修改了 MainActivity 的代码,所以会走到:
if(updateMode ==UPDATE_MODE_WARM_SWAP) {
...
Restarter.restartActivityOnUiThread(activity);
}
进而会辗转走到:
Restarter#restartActivity至此,MainActivity 得以重新加载,当运行到 onCreate() 的时候,此时 $change 成员已经不为空,且指向了 MainActivity$override 类的实例,也就是说 MainActivity#onCreate() 原有的逻辑已经执行不到了,而是执行了修改过后的逻辑,也就是 MainActivity$override#onCreate() 中的逻辑了。
三、最后
其中还涉及到资源相关的处理,这里就不罗列了。
Instant-Run 的好处就是只会把有修改的地方打成 dex 传到手机进行部署,主要是节约了开发时间;
这里强烈推荐一下阿里的 freeline,也是非常不错的一个及时部署得工具,有个缺陷就是会侵入我们的业务代码,比如需要在 Application 中进行初始化,不过这都好办,比如通过判断是否是 debug 包来初始化;同时通过增加 Property 来判断是否需要在 build.gradle 中应用插件。