【转载】InstantRun 原理——深度剖析 AndroidStudio 2.0
一、前言
Android Studio 2.0开始支持 Instant Run 特性, 使得在开发过程中能快速将代码变化更新到设备上。之前,更新代码之后需要先编译一个完整的新Apk,卸载设备上已安装的这个 Apk (若有),再 push 到设备安装,再启动。有了 Instant Run 特性之后,只需要 push 一些增量到设备上,直接执行,可以为开发人员节省大量时间。当然 Instant Run 特征只在 debug 时有效,对发布 release 版没有任何影响。
Instant Run 通过 hot swap, warm swap, code swap 三种 swap 来实现。Android Studio 会根据代码的改变自动决定 push 哪种 swap 到设备上,并根据不同的 swap 执行不同的行为。
代码改变内容 | Instant Run 行为 |
---|---|
修改一个实例方法或者一个静态方法的实现 | hot swap: 这是最快的情况,下次调用该方法时直接使用更新后的方法 |
修改或者删除一个资源 | warm swap: App 保护运行状态,但是会自动重启 activity, 所以屏幕会闪一下 |
|
|
|
需要重新编译整个App |
下面分析 Instant Run 的实现原理。Instant Run 特性是通过 gradle plugin (版本大于2.0)与 instant-run.jar 来实现的。gradle plugin 会对dex作一些必要的修改, 而 instant-run.jar 会被编译进 dex, 在运行的时候执行相应的功能。
二、第一次编译时对Apk的修改
<!-- 若 Apk 无 Application, 则在 AndroidManifest 中增加:-->
<application
android:name="com.android.tools.fd.runtime.BootstrapApplication"
...... >
<!-- 若 Apk 已经有 Application,则更改为: -->
<application
android:name="com.android.tools.fd.runtime.BootstrapApplication"
name="com.testbugrpt.MyApplication"
...... >
自动增加依赖 Jar 包,instant-run.jar
先介绍几个类,剩下的后面遇到再介绍:
Server:主要是建立一个 socket 服务器等待 Android Studio 的连接。Anroid Studio 发送相关的命令及数据(hot, warm, cold..., 命令字定义在 ProtocolConstants 中),Server 接收并执行,实现 Instant Run。
IncrementalChange:是一个接口:
public interface IncrementalChange {
Object access$dispatch(String arg1, Object[] arg2);
}
AppInfo:含有一些基本信息:
BootstrapApplication, 由于被设置成了 Apk 的 Application,所以 App 启动时最早得到执行机会:
首先会执行 attachBaseContext 方法:
在 setupClassLoaders 方法中,new 一个 IncrementalClassLoader, 用这个 loader 加载补丁 dex, 并将这个loader设置为当前 classloader 的 parent, 最后启动 server。
初始化 apk 原本 application, 并执行 attachBaseContext 方法。
再执行onCreate方法:
MonkeyPatcher.mokeyPatchApplication 主要是通过反射调用各种未导出的方法,将 ActivityThread 中的一些关于 application 的变量由 BootstrapApplication 更改为原 apk 的 application, 这一步与加壳中的逻辑是一样的。
MonkeyPatcher.MonkeyPathExisingResources 处理资源相关内容, 后面再详细介绍。
写个简单的类GenString:
public class GenString {
public String genString (int i){
return String.valueOf(i);
}
public static String genString2(int i){
return String.valueOf(i);
}
}
反编译生成的 Debug 版本 Apk, 分析对应的 GenString.smali:
增加静态变量:
# static fields
.field public static volatile synthetic $change:Lcom/android/tools/fd/runtime/IncrementalChange;
方法 genString ( genString2 类似)被更改为:
public String genString(int i) {
Object v0_1;
IncrementalChange v0 = GenString.$change;
if(v0 != null) {
v0_1 = v0.access$dispatch("genString.(I)Ljava/lang/String;", new Object[]{this, new Integer(i)});
} else {
String v0_2 = String.valueOf(i);
}
return ((String)v0_1);
}
可以看到整个函数的流程被类静态变量 $change 控制,若 $change 为 null,则执行原始逻辑;若 $change 不为 null, 则执行 $change.access$dispatch 方法,该方法的第一个参数为 getString 方法的签名,第二个参数为一个数组,用于放置getString 的所有参数。
第一次运行时,$change 会被设置为 null, 所以就是执行原始逻辑。当有 genString 有更改并启动 instant run 时,$change 就会被赋值,这样当执行到 genString 时,会调用 $change.access$dispath 方法。
下面分析这个 $change 何时会被设置,会被设置成什么, 以及 access$dispatch 的实现。
三、更改 Java 代码时, Instant Run 的执行流程
现在更改 genString2 方法为:
public static String genString2(int i){
return "helloworld_" + String.valueOf(i);
}
instant run之后,发现/data/data/com.testbugrpt/files/instant-run/dex-temp目录下增加文件:
-rw------- u0_a239 u0_a239 2276 2016-04-19 02:25 reload0x0000.dex
可以发现这个Dex几乎只是包含了被修改的类。
AppPatchesLoaderImpl中包含有所有被修改了代码的类名。
另外在被修改类的类名后面加上 $override 组成一个新 classname, 并实现 IncrementalChange 接口。在这个类中:
现在可以猜想到:上一节分析到的 $change 会被设置为 GenString$override, $change.access$dispath 会根据方法的签名调用对应的修改后的方法。下面确认这一过程。
Server接收到请求后:
请求数据包格式大概像这样子:
对于 hot swap
动态加载 Android Studio 传送过来的补丁 Dex, 并生成 com.android.tools.fd.runtime.AppPatchesLoaderImpl 的一个对象。由于 AppPatchesLoaderImpl 继承自 AbstractPatchesLoaderImpl, 所以上面的 load 方法其实是 AbstractPatchesLoaderImpl 的:
将每一个需要 patch 的 class 的静态变量 $change 设置为补丁 dex 中的 oriClassName$override。
这样,如果下次调用这些修改后的方法,就会因为 $change 为非空而调用 $change.access$dispath 方法, 这个方法通过第一个参数(即方法签名)从而确定到补丁 Dex 中的相应方法,最终实现 hot swap。
四、修改资源后,Instant Run 的执行流程
点击运行后,发现增加了一个文件:
root@g520:/data/data/com.testbugrpt/files/instant-run/right # ll
-rw------- u0_a240 u0_a240 231210 2016-04-20 01:53 resources.ap_
其实是一个 zip 资源包, 就像系统的 framework-res.apk 这个包里包含了当前的所有资源,包含了更新后的资源。
先简单介绍一下 Activity 获取资源时所涉及的几个主要数据结构(因系统版本不同有差异):
Activity 处理资源是通过 mResource 来进行的,而 Resource 会将这些操作都代理给 mAsset。
/system/framework/framework-res.apk 是系统资源,/data/app/xxx.apk 是 App 自身路径,表示可以访问自己本身的资源。如果把这个路径指向其它的资源路径,那就可以访问其它的资源了。Instant Run 热更新资源的思路就是,新建一个 AssetManager 对象,调用 addAssetPath 将 resources.ap_ 加到它的 path 上面。然后遍历所有的 Activity, 将每一个 Activity 的 mResource 中的 mAsset 设置为新建的 AssetManager 对象。
当然还有许多细节要处理。Activity中关于theme中的AssetManger对象也需要更新,需要清空当前Resource对象的cache等等。
这个实现代码主要在 MonkeyPatcher.MonkeyPathExisingResources。
五、其它
Instant Run可能存在的问题:
-
1、Instant Run 需要为 class 增加 method 来实现,若原 dex 的方法数量接近64K, 使用 Instant Run 大约将增加(140 + 3 * class个数)method, 导致超过64K上限,引起 build 出错。
-
2、如果已经使用 multi-dex, 而主 dex 方法接近65K, 也可能导致 build 出错。
-
3、multiprocess 时,可能禁用 instant run。
没有分析到的细节:
-
1、instant run 为了屏蔽系统之间的差异,做了许多兼容性处理。
-
2、instant run 对 native so 的处理。
-
3、在 multi-dex 环境下,instant run 的处理。
-
4、warm swap, cold swap 细节。
参考文献:
1、http://jiajixin.cn/2015/11/25/instant-run/
2、http://tools.android.com/tech-docs/instant-run
3、https://iobservable.net/blog/2016/03/20/how-android-instant-run-works/
4、http://mogu.io/117-117
5、http://blog.csdn.net/hknock/article/details/48003071
6、https://github.com/mmin18/LayoutCast
7、https://github.com/jasonross/Nuwa