Instant Run浅析(三)

我们知道,Android热修复的补丁通常是通过网络下发的,并且加载补丁后要重启进程才能生效。那么Instant Run的补丁是如何下发并且生效的呢?

简介

先看这张图:

图中的base.apk是首次运行应用时生成的apk包,reload.dex是接下来Instant Run运行生成的补丁包(HelloWorld$override.class被打包成reload.dex)。

前面两篇文章分别就图中某些问题进行了讨论:

本篇要讨论的是补丁包如何被发送、接收以及加载。

为便于理解问题,先直接给出几个相关的知识点:

  • 首先必须了解的是,Instant Run是CS架构的,它分为服务端和客户端
  • Instant Run的客户端运行在Android Studio中
  • Instant Run的服务端运行在手机上的app进程

也就是说,当你看见Android Studio的黄色闪电图标(Apply Change)亮起时,可以理解为Instant Run客户端成功连接/登录到Instant Run服务端,就像QQ客户端成功连接/登录QQ服务器端。这个比喻不完全准确,但二者并无本质上的不同。Instant Run客户端向Instant Run服务端发送消息,服务端处理消息并返回结果。

为了更直观地理解Instant Run,先看一个基于instant-run-client库开发的demo,代码见Github。这个demo有几个功能:

  • 让app弹出toast
  • 让app重启当前activity
  • 手工版的hot swap

demo的玩法如下:

  • 下载demo源码Github
  • Android Studio中运行demo的app module,确认黄色闪电图标亮起
  • 从上一步生成的build-info.xml文件找到token (一长串数字)
  • 下载并运行appclient.jar,运行方法java -jar appclient.jar

注:

  • build-info.xml文件的具体路径是/build/intermediates/build-info/debug/build-info.xml
  • appclient.jar编译自demo源码中的appclient module,编译方法./gradlew shadowJar

视频中demo发出”put patches”指令后紧接着发出”restart activity”,原来的”hello”变成了”你好”。很神奇?一起来看看背后的原理吧。

架构分析

上述demo是基于instant-run-client库实现的。instant-run-clientinstant-run-server的关系大致如下图:

  • Android Gradle Plugin生成patch
  • instant-run-client发送patch
  • instant-run-server接收并加载patch

Instant Run的源码在ASOP的platform/tools/base/目录。base项目是ASOP中一个独立的git工程。注意,base项目无法独立编译。本文基于该项目studio-master-dev分支进行分支,最后一次提交是ec8e2b78ea42ac57d6e63bf88a3e09e1713bcc08

代码结构很清晰,它分成以下几个模块:

Instant Run各模块及主要类总结如下:

模块 类名 功能
common ProtocolConstants instant run协议
common ApplicationPatch 对patch的路径及数据的封装
runtime IncrementalChange 关键的接口,代表代码变更
runtime PatchesLoader 关键接口,用于加载patch,具体见AbstractPatchesLoaderImpl
server Server 服务端的核心
client UpdateMode app加载patch后的启动模式
client InstantRunBuildInfo 对应于build-info.xml文件
client InstantRunClient 客户端的核心

注意这里的两个重点:

  • Instant Run服务端被打包到APK
  • Instant Run客户端被集成到Android Studio

如果你不太了解build-info.xml,可以参考这里:

另外,server模块的InstantRunContentProvider很有意思,有必要提一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
public final class InstantRunContentProvider extends ContentProvider {

@Override
public boolean onCreate() {
if (isMainProcess()) {
Log.i(Logging.LOG_TAG, "starting instant run server: is main process");
Server.create(getContext());
} else {
Log.i(Logging.LOG_TAG, "not starting instant run server: not main process");
}
return true;
}
}

反编译base.apk你会发现构建系统偷偷帮你添加了InstantRunContentProvider,这么做的原因是利用ContentProvider.onCreate()早于Application.onCreate()运行的特点来简化Instant Run的实现。2016年Google推出Instant Run,在早期版本中为了能将Instant Run应用于Application,会向app中注入BootstrapApplication,这个BootstrapApplication代理到app原来的ApplicationBootstrapApplication方案带来不必要的复杂性,后期版本中采用InstantRunContentProvider方案。2016年关于Instant Run的技术博客中,比如这篇,经常可以看到BootstrapApplication,请注意避免踩坑。

如果你不相信providerapplication先运行,可以看看日志。

源码分析

Instant Run客户端通过ddmlib库跟服务端通信。使用ddmlib时在build.gradle添加如下依赖:

1
2
// https://mvnrepository.com/artifact/com.android.tools.ddms/ddmlib
compile 'com.android.tools.ddms:ddmlib:26.2.1'

Instant Run客户端

instant-run-client模块并不复杂,这是它的类图。

instant-run-client最终会调用ServiceCommunicator.talkToService(IDevice device, Communicator<T> communicator)跟Android设备通信的。

  • device参数通过ddmlib库获取
  • 使用ddmlib获取device列表的方法可参考这里
  • 通信前device通过本地的TCP 46622端口跟远程的Unix Domain Socket建立端口转发关系
  • communicator参数封装了通信逻辑,ProtocolConstants中的每个命令字都有对应的Communicator实现

InstantRunClient有如下几个重要方法:

  • getAppState()
  • showToast()
  • restartActivity()
  • pushPatches()

鉴于这些方法非常容易理解,这里就不做无聊的分析。有兴趣可以参考前面提到的demo

Instant Run服务端

Server是Instant Run服务端的核心,它是一个典型的服务端程序

不过跟常见的服务端程序有所不同的是,Server使用LocalServerSocket以及LocalSocket编程,而不是Java中常用的ServerSocket以及Socket编程。

查文档知道LocalServerSocketLocalSocket是Android平台特有的类,它们与Socket的区别可参考这里

Server的核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private class SocketServerThread extends Thread {
@Override
public void run() {
while (true) {
try {
LocalServerSocket serverSocket = Server.this.serverSocket;
if (serverSocket == null) {
break; // stopped?
}
LocalSocket socket = serverSocket.accept();
SocketServerReplyThread socketServerReplyThread = new SocketServerReplyThread(
socket);
...
} catch (Throwable e) {
...
}
}
}
}

这种代码结构是不是很眼熟?总结一下它的套路:

  1. 启动SocketServerThread线程
  2. SocketServerThread线程进入无限循环
  3. accept()监听客户端连接
  4. 启动SocketServerReplyThread线程处理客户端连接
  5. 跳转到2,继续循环

SocketServerReplyThread.handle()方法负责处理客户端消息,我们感兴趣的消息指令见下表。

命令字 功能
MESSAGE_PING 检查服务端在线状态。 服务端在线时Android Studio的黄色闪电图标亮起
MESSAGE_SHOW_TOAST 通知服务端弹出Toast提示
MESSAGE_RESTART_ACTIVITY 通知服务端重启当前activity 。某些修改必须重起activity才能生效
MESSAGE_PATCHES 向服务端发送patch

MESSAGE_PATCHES的处理流程如下:

再来看对应的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
private void handle(DataInputStream input, DataOutputStream output) throws IOException {

case MESSAGE_PATCHES: {
if (!authenticate(input)) {
return;
}

List<ApplicationPatch> changes = ApplicationPatch.read(input);
if (changes == null) {
continue;
}

boolean hasResources = hasResources(changes);
// 第1处,初始的 updateMode 值由客户端发过来
int updateMode = input.readInt();
// 第2处, 服务端重新计算updateMode
updateMode = handlePatches(changes, hasResources, updateMode);

boolean showToast = input.readBoolean();

// Send an "ack" back to the IDE; this is used for timing purposes only
output.writeBoolean(true);

restart(updateMode, hasResources, showToast);
continue;
}
}

private int handlePatches(@NonNull List<ApplicationPatch> changes, boolean hasResources,
int updateMode) {
if (hasResources) {
FileManager.startUpdate();
}

for (ApplicationPatch change : changes) {
String path = change.getPath();
// 第3处,更新代码或更新资源
if (path.equals(RELOAD_DEX_FILE_NAME)) {
updateMode = handleHotSwapPatch(updateMode, change);
} else if (isResourcePath(path)) {
updateMode = handleResourcePatch(updateMode, change, path);
}
}

if (hasResources) {
FileManager.finishUpdate(true);
}

return updateMode;
}

private static int handleResourcePatch(int updateMode, @NonNull ApplicationPatch patch,
@NonNull String path) {
if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) {
Log.v(Logging.LOG_TAG, "Received resource changes (" + path + ")");
}
FileManager.writeAaptResources(path, patch.getBytes());
//noinspection ResourceType
// 第4处代码,资源更新时,updateMode至少是WARM_SWAP
updateMode = Math.max(updateMode, UPDATE_MODE_WARM_SWAP);
return updateMode;
}

注意第3处代码,它在更新资源或代码。更新代码时会调用handleHotSwapPatch(),它是加载patch的关键所在,代码如下(为了保持代码清晰这里仅保留主要部分):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
 private int handleHotSwapPatch(int updateMode, @NonNull ApplicationPatch patch) {
try {
String dexFile = FileManager.writeTempDexFile(patch.getBytes());
String nativeLibraryPath = FileManager.getNativeLibraryFolder().getPath();
// 第5处,从dex文件加载指定类
DexClassLoader dexClassLoader = new DexClassLoader(dexFile,
context.getCacheDir().getPath(), nativeLibraryPath,
getClass().getClassLoader());

// we should transform this process with an interface/impl
Class<?> aClass = Class.forName(
"com.android.tools.ir.runtime.AppPatchesLoaderImpl", true, dexClassLoader);
try {
// 第6处,创建loader实例并加载patch
PatchesLoader loader = (PatchesLoader) aClass.newInstance();
if (!loader.load()) {
// 第7处,更新updateMode
updateMode = UPDATE_MODE_COLD_SWAP;
}
} catch (Exception e) {
Log.e(Logging.LOG_TAG, "Couldn't apply code changes", e);
// 第8处,更新updateMode
updateMode = UPDATE_MODE_COLD_SWAP;
}
} catch (Throwable e) {
Log.e(Logging.LOG_TAG, "Couldn't apply code changes", e);
// 第9处,更新updateMode
updateMode = UPDATE_MODE_COLD_SWAP;

return updateMode;
}
  • 第5处代码:从dex文件中加载AppPatchesLoaderImpl
  • 第6处代码:创建loader实例并加载patch
  • 第7,8,9处代码:loader加载patch不成功或者发生异常时,updateMode直接升级为COLD_SWAP

关于这段代码还应注意的是classloader之间是如何建立关系的:

上面的代码中使用AppPatchesLoaderImpl加载其他类。AppPatchesLoaderImpl由Android Gradle Plugin生成,我们看不到源码。不过可以在build/intermediates/transforms/instantRun/debug目录找对应的类文件,其反编译结果如下。

getPatchedClasses()方法返回的String[]记录了本次修改的类。就本次而言,仅修改了HelloWorld

AbstractPatchesLoaderImplAppPatchesLoaderImpl的父类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public abstract class AbstractPatchesLoaderImpl implements PatchesLoader {
// 这个方法用于加载patch
@Override
public boolean load() {
for (String className : getPatchedClasses()) {
try {
ClassLoader cl = getClass().getClassLoader();
Class<?> aClass = cl.loadClass(className + "$override");
Object o = aClass.newInstance();

Class<?> originalClass = cl.loadClass(className);
Field changeField = originalClass.getDeclaredField("$change");
// force the field accessibility as the class might not be "visible"
// from this package.
changeField.setAccessible(true);

Object previous =
originalClass.isInterface()
? patchInterface(changeField, o)
: patchClass(changeField, o);

} catch (Exception e) {
return false;
}
}
return true;
}

// 这个方法用于指定要加载哪些patch类
public abstract String[] getPatchedClasses();
}

AbstractPatchesLoaderImpl.load()用于加载patch。以HelloWorld为例,load()方法的几个关键操作如下:

  • 加载HelloWorld$override
  • 创建HelloWorld$override实例
  • 找到HelloWorld类的$change字段
  • HelloWorld$override实例设置到$change字段。参考Instant Run浅析(二)

补充知识

这里记录了一些有用的知识点,分析Instant Run源码时可能会用到。

updateMode

InstantRunClient.pushPatches()用于发送patch,这个方法返回UpdateMode。通过pushPatches()的代码容易明白updateMode是由Android gradle plugin,instant-run-server以及Android Studio三方共同确定的 ,instant-run-client 充当协调者

用图画出来大概是这样:

总结一下就是以下三个影响因素,其中最重要的是build-info.xml

  • build-info.xml - Android gradle plugin生成的build-info.xml (文件路径/build/intermediates/build-info/debug/build-info.xml)
  • app状态信息 - 运行状态、前台状态以及 AbstractPatchesLoaderImpl.load() 返回值
  • Android Studio instant run设置 - 见下图

instant-run-client作为协调者综合考量这三个因素,得到最终的updateMode,而最终的updateMode决定app是否会被重启。

build-info.xml

Android Studio开启instant run之后,每次instant run的编译信息记录在/build/intermediates/build-info/debug/build-info.xml文件中。文件内容类似这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="utf-8"?>
<instant-run
abi="x86,armeabi-v7a,armeabi"
api-level="24"
build-mode="COLD"
density="420dpi"
format="10"
ir-eligibility="SYNTHETIC_CONSTRUCTOR_CHANGE"
plugin-version="3.1.3"
timestamp="1537942541795"
token="-1881962896096570399"
verifier="COLD_SWAP_REQUESTED" >

<build
build-mode="COLD"
ir-eligibility="SYNTHETIC_CONSTRUCTOR_CHANGE"
timestamp="1537942541795"
verifier="COLD_SWAP_REQUESTED" >
<artifact
location="/Users/build/intermediates/split-apk/debug/slices/slice_2.apk"
type="SPLIT" />
</build>

</instant-run>

直接看xml文件会有些无聊,结合代码容易理解一些。

  • InstantRunBuildInfo - instant run编译信息,对应于build-info.xml<instant-run>节点
  • InstantRunArtifact - instant run编译结果,对应于build-info.xml<artifact>节点。编译结果本质上文件
  • InstantRunArtifactType - instant run编译结果的类型,对应于build-info.xml<artifact>节点的type属性

InstantRunArtifactType分为以下几种类型:

  • MAIN - 包含资源文件的主APK
  • SPLIT_MAIN - 包含资源文件的主APK,适用于分包(多个APK文件)的场景
  • SPLIT - 分包APK
  • RELOAD_DEX - 代码更新包
  • RESOURCES - 资源更新包

instant run会进行根据情况进行不同类型的构建,每次构建会生成一个<build>节点保存到build-info.xml中:

  • FULL - 全量构建,一般发生在首次运行instant run时。生成一个类型为SPLIT_MAIN的apk文件以及一个或多个类型为SPLIT的apk文件
  • HOT_WARM - 一般发生在进行了兼容的改动之后。生成类型为 RELOAD_DEX 的dex文件,或者类型为RESOURCES的ap_文件
  • COLD - 一般发生在进行了不兼容的改动后。生成类型为SPLIT的文件

build-info.xml中还会记录两个关键信息:

  • duration - 最后一次构建时各任务的耗时信息
  • ir-eligibility - 违反哪条规则导致COLD构建。比如修改方法签名会导致COLD构建,这时的ir-eligibility是METHOD_DELETED

当你发生instant run运行慢或者行为不符合预期,build-info.xml或许能提供一些线索。

1
2
3
4
5
6
<instant-run build-mode="COLD" ir-eligibility="METHOD_DELETED" verifier="COLD_SWAP_REQUESTED">
<task name="javac" duration="0" />
<task name="instant-run-dex" duration="0" />
<task name="instant-run-transform" duration="0" />
<task name="verifier" duration="0" />
</instant-run>

使用如下命令在platform/tools/base/工程中搜索”build-info”字符串:

1
find . -name "*.java" | xargs grep "build-info"

可以找到几个跟build-info.xml相关的类:

  • BuildInfoWriterTask
  • ApplicationTaskManager
  • InstantRunBuildContext

日志

这里介绍几个跟Instant Run相关的重要日志。

Instant Run日志

小技巧:instant run打印了非常详尽的日志。打开日志后运行代码,对照着日志和运行结果分析代码,看”动”的代码比看”静止”的代码更简单。adb shell中执行setprop log.tag.InstantRun VERBSOE命令可以打开instant run日志。

instant run日志类似这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
09-27 18:16:25.766 7749-7867 V/InstantRun: Received Ping message from the IDE; returned active = true
09-27 18:16:25.769 7749-7867 V/InstantRun: Received EOF from the IDE
09-27 18:16:25.783 7749-7867 V/InstantRun: Received connection from IDE: spawning connection thread
09-27 18:16:25.787 7749-7867 V/InstantRun: Received incremental code patch
09-27 18:16:25.794 7749-7867 V/InstantRun: Writing new dex file: /data/data/com.sunmoonblog.demo_screenshot/files/instant-run/dex-temp/reload0x0004.dex
09-27 18:16:25.798 7749-7867 V/InstantRun: Reading live code from /data/data/com.sunmoonblog.demo_screenshot/files/instant-run/dex-temp/reload0x0004.dex
09-27 18:16:25.978 7749-7867 V/InstantRun: Got the patcher class class com.android.tools.ir.runtime.AppPatchesLoaderImpl
Got the patcher instance com.android.tools.ir.runtime.AppPatchesLoaderImpl@1314ec2
Got the list of classes
class com.sunmoonblog.demo_screenshot.MainActivity
09-27 18:16:25.982 7749-7867 V/InstantRun: Finished loading changes; update mode =1
Applying incremental code without restart
09-27 18:16:25.984 7749-7867 V/InstantRun: About to show toast for activity com.sunmoonblog.demo_screenshot.MainActivity@665fe86: Applied code changes without activity restart
Received EOF from the IDE

Android Studio日志

Android Studio日志 - 在Android Studio - Help - Show Log in Finder中可以找到这些日志。强烈推荐看看官方介绍

其中idea.log是Android Studio以及SDK运行时生成的日志,在这里可以找到build-info.xml中build-mode的根源。

其他日志

idea.log同级别有一个flr目录,其中包括几个重要日志。

  • build.log - This log captures build information from Gradle。这个日志跟Android Studio build工具栏中看到的日志是一样的,跟gradle带–info参数运行时看到的日志也是一样的。
  • build-info.xml - 这个日志我们之前分析过,官方对它的介绍是这样的: This is an intermediate file that Android Studio uses to coordinate Instant Run features with the Android plugin for Gradle
  • profile.log - This log provides a profile of how quickly Gradle progressed through each build task.

注:你可能觉得这么既然Android Studio build工具栏(第1种方式)或gradle --info(第2种方式)者能看到日志,何必要那么麻烦去Android Studio的日志目录中找build.log文件。问题在于日志往往较多,第1种方式无法显示完整日志,不便于排查问题。而第2种方式压根不能进行instant run

1
2
3
4
5
6
7
2018-09-30 10:37:10,325 [e-1024-b01]   INFO -                    #InstantRun - Invoking hotswap launch 
2018-09-30 10:37:11,216 [thread 878] INFO - #InstantRun - Build timestamps: Local: 1538274511551, Device: 1538274511551
2018-09-30 10:37:11,247 [thread 878] INFO - #InstantRun - Ping sent and replied successfully, application seems to be running. Foreground=true
2018-09-30 10:37:11,267 [thread 878] INFO - #InstantRun - Ping sent and replied successfully, application seems to be running. Foreground=true
2018-09-30 10:37:11,924 [thread 878] INFO - ools.idea.fd.InstantRunBuilder - BuildCause: APP_USES_MULTIPLE_PROCESSES, BuildMode: COLD
...
2018-09-30 10:37:45,834 [e-1024-b01] INFO - ild.invoker.GradleBuildInvoker - Gradle build finished in 33s 900ms

如果找不到上述日志,可以检查一下日志开关是否开启:

总结

简单总结一下本文内容:

  • Instant Run是CS架构
  • Android Studio作为客户端,app作为服务端
  • 补丁包是由Android Studio发送到app
  • app加载hotswap类型补丁时不重启即可生效
  • PatchesLoader从补丁包加载HelloWorld$override类并创建其实例
  • HelloWorld$override实例设置到HelloWorld.$change字段
  • demo演示了instant-run-client库的用法,demo见Github