前言
Android Studio 从2.0开始就支持了Instant Run ,大大减少了应用编译时长,提升了开发效率。本文将结合Instant Run的源码抛砖引玉,揭开Instant Run 背后的秘密。如有不当之处,烦请不吝赐教。contact me :bboylin24@gmail.com 本文为作者原创,转载须注明出处。
OverView
Instant Run将一次增量编译划分为三种类型:
- Hot Swap : 仅仅是方法内部逻辑的更改,无需重启activity或者app,即时生效。
- Warm Swap : 涉及到resource的修改。需要重启activity才能生效。
- Cold Swap :涉及到类结构的更改,例如新增了一个方法,或者方法签名修改了。需要重启app。
整体的思路是埋点,在每个类实例中插入一个IncrementalChange类型的$change字段,每个方法前插入一段判断逻辑,如果$change不为null,就把方法调用重定向到$change的对应方法上。假如方法逻辑有更新就更新辅助类,从而达到无需重启即可应用更改的能力。有些热修复框架也借鉴了这个思路。
为什么这样设计呢?直接更新这个类(计作A)不行吗?我们知道Java语言规范:
- 同一个类不能被reload
- 判定两个类相同不仅要fully qualified name相同,而且class loader也必须同一个。否则会有ClassCastException。
基于此:当A的实现变了的话无法实时更新。但是更新辅助类A$override就没有问题,因为A$override都实现了IncrementalChange接口,所以使用新的class loader去加载更新后的A$override实例并赋值给$change是没有问题的。这也是一种代理。
一些热修复和插件化框架也借鉴了资源的Warm Swap原理。主要是生成一个新的包含更新后的资源的AssetManager反射替换掉原先的AssetManager.
具体的细节后文再结合源码分析。
源码分析
InstantRun的设计遵循C/S模型,源码分以下几个层级:
其中client负责将studio build产生的增量产物转化成patch数据通过socket传输给server,runtime是会打进apk的运行期会使用到的类,common是几个都会用的类和常量,server是在app上运行的,负责接受patch数据并且apply patch数据。annotation只有DisableInstantRun
这一个注解,标记方法,类或包不使用instant-run:1
2
3
4
5
6
7
8
9/**
* Annotate a method, a class, a package when InstantRun should be disabled for the annotated
* element.
*/
(RetentionPolicy.CLASS)
({PACKAGE, TYPE, CONSTRUCTOR, METHOD})
public DisableInstantRun {
}
我们点Android studio run这个按钮右边的⚡️就可以使用InstantRun应用最新的更改,前提是在Preferences里开了enable InstantRun。我们来看下,第一次点run发生了什么:1
2
3
4
5
6
7
8adb install-multiple -r -t /Users/denglin03/Desktop/projects/DemoApp/app/build/intermediates/split-apk/debug/slices/slice_0.apk /Users/denglin03/Desktop/projects/DemoApp/app/build/intermediates/split-apk/debug/slices/slice_3.apk /Users/denglin03/Desktop/projects/DemoApp/app/build/intermediates/split-apk/debug/slices/slice_8.apk /Users/denglin03/Desktop/projects/DemoApp/app/build/intermediates/split-apk/debug/slices/slice_9.apk /Users/denglin03/Desktop/projects/DemoApp/app/build/intermediates/resources/instant-run/debug/resources-debug.apk /Users/denglin03/Desktop/projects/DemoApp/app/build/intermediates/split-apk/debug/dep/dependencies.apk /Users/denglin03/Desktop/projects/DemoApp/app/build/intermediates/split-apk/debug/slices/slice_2.apk /Users/denglin03/Desktop/projects/DemoApp/app/build/intermediates/split-apk/debug/slices/slice_7.apk /Users/denglin03/Desktop/projects/DemoApp/app/build/intermediates/split-apk/debug/slices/slice_5.apk /Users/denglin03/Desktop/projects/DemoApp/app/build/intermediates/split-apk/debug/slices/slice_4.apk /Users/denglin03/Desktop/projects/DemoApp/app/build/intermediates/split-apk/debug/slices/slice_6.apk /Users/denglin03/Desktop/projects/DemoApp/app/build/intermediates/split-apk/debug/slices/slice_1.apk /Users/denglin03/Desktop/projects/DemoApp/app/build/intermediates/instant-run-apk/debug/app-debug.apk
Split APKs installed
$ adb shell am start -n "com.baidu.swan/com.baidu.swan.MainActivity" -a android.intent.action.MAIN -c android.intent.category.LAUNCHER
Client not ready yet..Waiting for process to come online
Connected to process 3519 on device 8cd2bb1b
Capturing and displaying logcat messages from application. This behavior can be disabled in the "Logcat output" section of the "Debugger" settings page.
W/ResourceType: No package identifier when getting name for resource number 0x00000000
I/InstantRun: starting instant run server: is main process
可以看到不同于正常的run流程,启用了instantRun之后一次安装了app-debug.apk, dependencies.apk, resources-debug.apk,以及10个slice apk。可以看到app-debug.apk完全没有我们写的代码,主要是IR(InstantRun简称,后同) server的代码;dependencies.apk主要是依赖的framework的代码;resources-debug.apk里没有dex,都是资源文件;slice apk里是我们真正写的代码。可以看到这些apk里找不到我们所说的带$override
后缀的class。后面我们再说。
Hot Swap
在transform下的instantrun目录可以找到transform后的MainActivity: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
59public class MainActivity extends AppCompatActivity {
public static final long serialVersionUID = -6288346222584639322L;
public MainActivity() {
IncrementalChange var1 = $change;
if (var1 != null) {
Object[] var10001 = (Object[])var1.access$dispatch("init$args.([Lcom/baidu/swan/MainActivity;[Ljava/lang/Object;)Ljava/lang/Object;", new Object[]{null, new Object[0]});
Object[] var2 = (Object[])var10001[0];
this(var10001, (InstantReloadException)null);
var2[0] = this;
var1.access$dispatch("init$body.(Lcom/baidu/swan/MainActivity;[Ljava/lang/Object;)V", var2);
} else {
super();
}
}
public void onCreate(Bundle savedInstanceState) {
IncrementalChange var2 = $change;
if (var2 != null) {
var2.access$dispatch("onCreate.(Landroid/os/Bundle;)V", new Object[]{this, savedInstanceState});
} else {
super.onCreate(savedInstanceState);
this.setContentView(2131296284);
}
}
public void onResume() {
IncrementalChange var1 = $change;
if (var1 != null) {
var1.access$dispatch("onResume.()V", new Object[]{this});
} else {
super.onResume();
this.startAction();
}
}
public void startAction() {
IncrementalChange var1 = $change;
if (var1 != null) {
var1.access$dispatch("startAction.()V", new Object[]{this});
} else {
(new DemoAction()).start();
}
}
MainActivity(Object[] var1, InstantReloadException var2) {
String var3 = (String)var1[1];
switch(var3.hashCode()) {
case -2089128195:
super();
return;
case 247605451:
this();
return;
default:
throw new InstantReloadException(String.format("String switch could not find '%s' with hashcode %s in %s", var3, var3.hashCode(), "com/baidu/swan/MainActivity"));
}
}
}
很容易看出来,还少了一个$change字段的定义,从slice apk里MainActivity的字节码可以看出1
2# static fields
.field public static volatile transient synthetic $change:Lcom/android/tools/ir/runtime/IncrementalChange; = null
$change这个字段是volatile transient synthetic的。synthetic是属性表中的一种定长属性,凡是不出现在源码中的字段必须用synthetic标识。并且$change是IncrementalChange类型。
从生成的代码可以知道,当$change不为null的时候,所有方法会代理到$change的access$dispatch方法。例如onCreate调的是:access$dispatch("onCreate.(Landroid/os/Bundle;)V", new Object[]{this, savedInstanceState});
。我们打断点可以验证,其实是调的MainActivity$override的方法。
在transform下的InstantRun目录下可以看到MainActivity$override的源码,实现了IncrementalChange接口。
1 | public class MainActivity$override implements IncrementalChange { |
可以知道对MainActivity任意方法的调用会代理到MainActivity$override的access$dispatch方法,传入一个类似方法签名的字符串,access$dispatch根据这个字符串的hashcode分发到MainActivity$override对应的方法中。
下面来看下IR源码,从client的pushPatches方法开始:
1 | public UpdateMode pushPatches(@NonNull IDevice device, |
可知针对资源的更新采取的是warmSwap,针对reload dex,如果app在前台,产生一个hot swap 的patch;否则抛异常InstantRunPushFailedException:”Can’t apply hot swap patch: app is no longer running”。得到所有的更改patches后调用另一个pushPatches方法: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
53public UpdateMode pushPatches(@NonNull IDevice device,
@NonNull final String buildId,
@NonNull final List<ApplicationPatch> changes,
@NonNull UpdateMode updateMode,
final boolean isRestartActivity,
final boolean isShowToastEnabled) throws IOException {
if (changes.isEmpty() || updateMode == UpdateMode.NO_CHANGES) {
// Sync the build id to the device; Gradle might rev the build id
// even when there are no changes, and we need to make sure that the
// device id reflects this new build id, or the next build will
// discover different id's and will conclude that it needs to do a
// full rebuild
transferLocalIdToDeviceId(device, buildId);
return UpdateMode.NO_CHANGES;
}
if (updateMode == UpdateMode.HOT_SWAP && isRestartActivity) {
updateMode = updateMode.combine(UpdateMode.WARM_SWAP);
}
final UpdateMode updateMode1 = updateMode;
mAppService.talkToService(device, new Communicator<Boolean>() {
public Boolean communicate(@NonNull DataInputStream input,
@NonNull DataOutputStream output) throws IOException {
output.writeInt(MESSAGE_PATCHES);
writeToken(output);
ApplicationPatchUtil.write(output, changes, updateMode1);
// Let the app know whether it should show toasts
output.writeBoolean(isShowToastEnabled);
// Finally read a boolean back from the other side; this has the net effect of
// waiting until applying/verifying code on the other side is done. (It doesn't
// count the actual restart time, but for activity restarts it's typically instant,
// and for cold starts we have no easy way to handle it (the process will die and a
// new process come up; to measure that we'll need to work a lot harder.)
input.readBoolean();
return false;
}
int getTimeout() {
return 8000; // allow up to 8 seconds for resource push
}
});
transferLocalIdToDeviceId(device, buildId);
return updateMode;
}
调用了ServiceCommunicator的talkToService方法,使用socket将patch数据发送给server的socket。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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
private void handle(DataInputStream input, DataOutputStream output) throws IOException {
long magic = input.readLong();
if (magic != PROTOCOL_IDENTIFIER) {
Log.w(Logging.LOG_TAG, "Unrecognized header format " + Long.toHexString(magic));
return;
}
int version = input.readInt();
// Send current protocol version to the IDE so it can decide what to do
output.writeInt(PROTOCOL_VERSION);
if (version != PROTOCOL_VERSION) {
Log.w(
Logging.LOG_TAG,
"Mismatched protocol versions; app is "
+ "using version "
+ PROTOCOL_VERSION
+ " and tool is using version "
+ version);
return;
}
while (true) {
int message = input.readInt();
switch (message) {
case MESSAGE_EOF:
{
if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) {
Log.v(Logging.LOG_TAG, "Received EOF from the IDE");
}
return;
}
case MESSAGE_PING:
{
// Send an "ack" back to the IDE.
// The value of the boolean is true only when the app is in the
// foreground.
boolean active = Restarter.getForegroundActivity(context) != null;
output.writeBoolean(active);
if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) {
Log.v(
Logging.LOG_TAG,
"Received Ping message from the IDE; "
+ "returned active = "
+ active);
}
continue;
}
case MESSAGE_PATH_EXISTS:
{
if (FileManager.USE_EXTRACTED_RESOURCES) {
String path = input.readUTF();
long size = FileManager.getFileSize(path);
output.writeLong(size);
if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) {
Log.v(
Logging.LOG_TAG,
"Received path-exists("
+ path
+ ") from the "
+ "IDE; returned size="
+ size);
}
} else {
if (Log.isLoggable(Logging.LOG_TAG, Log.ERROR)) {
Log.e(Logging.LOG_TAG, "Unexpected message type: " + message);
}
}
continue;
}
case MESSAGE_PATH_CHECKSUM:
{
if (FileManager.USE_EXTRACTED_RESOURCES) {
long begin = System.currentTimeMillis();
String path = input.readUTF();
byte[] checksum = FileManager.getCheckSum(path);
if (checksum != null) {
output.writeInt(checksum.length);
output.write(checksum);
if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) {
long end = System.currentTimeMillis();
String hash = new BigInteger(1, checksum).toString(16);
Log.v(
Logging.LOG_TAG,
"Received checksum("
+ path
+ ") from the "
+ "IDE: took "
+ (end - begin)
+ "ms to compute "
+ hash);
}
} else {
output.writeInt(0);
if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) {
Log.v(
Logging.LOG_TAG,
"Received checksum("
+ path
+ ") from the "
+ "IDE: returning <null>");
}
}
} else {
if (Log.isLoggable(Logging.LOG_TAG, Log.ERROR)) {
Log.e(Logging.LOG_TAG, "Unexpected message type: " + message);
}
}
continue;
}
case MESSAGE_RESTART_ACTIVITY:
{
if (!authenticate(input)) {
return;
}
Activity activity = Restarter.getForegroundActivity(context);
if (activity != null) {
if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) {
Log.v(Logging.LOG_TAG, "Restarting activity per user request");
}
Restarter.restartActivityOnUiThread(activity);
}
continue;
}
case MESSAGE_PATCHES: {
if (!authenticate(input)) {
return;
}
List<ApplicationPatch> changes = ApplicationPatch.read(input);
if (changes == null) {
continue;
}
boolean hasResources = hasResources(changes);
int updateMode = input.readInt();
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;
}
case MESSAGE_SHOW_TOAST:
{
String text = input.readUTF();
Activity foreground = Restarter.getForegroundActivity(context);
if (foreground != null) {
Restarter.showToast(foreground, text);
} else if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) {
Log.v(
Logging.LOG_TAG,
"Couldn't show toast (no activity) : " + text);
}
continue;
}
default:
{
if (Log.isLoggable(Logging.LOG_TAG, Log.ERROR)) {
Log.e(Logging.LOG_TAG, "Unexpected message type: " + message);
}
// If we hit unexpected message types we can't really continue
// the conversation: we can misinterpret data for the unexpected
// command as separate messages with different meanings than intended
return;
}
}
}
}
当接收到MESSAGE_PATCHES类型的message时候就调用handlePatches:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21private int handlePatches(@NonNull List<ApplicationPatch> changes, boolean hasResources,
int updateMode) {
if (hasResources) {
FileManager.startUpdate();
}
for (ApplicationPatch change : changes) {
String path = change.getPath();
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;
}
hot swap调用的是handleHotSwapPatch1
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
private int handleHotSwapPatch(int updateMode, @NonNull ApplicationPatch patch) {
if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) {
Log.v(Logging.LOG_TAG, "Received incremental code patch");
}
try {
String dexFile = FileManager.writeTempDexFile(patch.getBytes());
if (dexFile == null) {
Log.e(Logging.LOG_TAG, "No file to write the code to");
return updateMode;
} else if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) {
Log.v(Logging.LOG_TAG, "Reading live code from " + dexFile);
}
String nativeLibraryPath = FileManager.getNativeLibraryFolder().getPath();
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 {
if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) {
Log.v(Logging.LOG_TAG, "Got the patcher class " + aClass);
}
PatchesLoader loader = (PatchesLoader) aClass.newInstance();
if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) {
Log.v(Logging.LOG_TAG, "Got the patcher instance " + loader);
}
String[] getPatchedClasses = (String[]) aClass
.getDeclaredMethod("getPatchedClasses").invoke(loader);
if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) {
Log.v(Logging.LOG_TAG, "Got the list of classes ");
for (String getPatchedClass : getPatchedClasses) {
Log.v(Logging.LOG_TAG, "class " + getPatchedClass);
}
}
if (!loader.load()) {
updateMode = UPDATE_MODE_COLD_SWAP;
}
} catch (Exception e) {
Log.e(Logging.LOG_TAG, "Couldn't apply code changes", e);
e.printStackTrace();
updateMode = UPDATE_MODE_COLD_SWAP;
}
} catch (Throwable e) {
Log.e(Logging.LOG_TAG, "Couldn't apply code changes", e);
updateMode = UPDATE_MODE_COLD_SWAP;
}
return updateMode;
}
首先将patch中的byte数组通过writeTempDexFile写到手机里,保存成/data/data/applicationId/files/instant-run/dex-temp下的最新的一个reload0x0000.dex这种格式的文件。0x后面的四位表示的数越大,这个文件越新。我们找到这个dex可以发现,其实就两个类,一个是transform的时候生成的AppPatchesLoaderImpl,另一个是因为我们更改了MainActivity导致的MainActivity$override相应更改。1
2
3
4
5
6
7
8
9
10public class AppPatchesLoaderImpl extends AbstractPatchesLoaderImpl {
public static final long BUILD_ID = 1548383899788L;
public AppPatchesLoaderImpl() {
}
public String[] getPatchedClasses() {
return new String[]{"com.baidu.swan.MainActivity"};
}
}
getPatchedClasses实际上是一个override的方法,返回修改过的类名。
接着往下看,创建了一个DexClassLoader实例,然后利用它加载AppPatchesLoaderImpl这个类,反射创建AppPatchesLoaderImpl实例,调用getPatchedClasses方法。接着调用load方法。我们来看AbstractPatchesLoaderImpl的load方法: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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77public abstract String[] getPatchedClasses();
public boolean load() {
for (String className : getPatchedClasses()) {
try {
// 获取AppPatchesLoaderImpl的classLoader,通过前一步可知是新建的DexClassLoader实例
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);
// If there was a previous change set, mark it as obsolete:
if (previous != null) {
Field isObsolete = previous.getClass().getDeclaredField("$obsolete");
if (isObsolete != null) {
isObsolete.set(null, true);
}
}
if (logging != null && logging.isLoggable(Level.FINE)) {
logging.log(Level.FINE, String.format("patched %s", className));
}
} catch (Exception e) {
if (logging != null) {
logging.log(
Level.SEVERE,
String.format("Exception while patching %s", className),
e);
}
return false;
}
}
return true;
}
/**
* When dealing with interfaces, the $change field is a final {@link
* java.util.concurrent.atomic.AtomicReference} instance which contains the current patch class
* or null if it was never patched.
*
* @param changeField the $change field.
* @param patch the patch class instance.
* @return the previous patch instance.
*/
private Object patchInterface(Field changeField, Object patch)
throws IllegalAccessException, NoSuchMethodException, InvocationTargetException {
Object atomicReference = changeField.get(null);
Object previous = get.invoke(atomicReference);
set.invoke(atomicReference, patch);
return previous;
}
/**
* When dealing with classes, the $change field is the patched class instance or null if it was
* never patched.
*
* @param changeField the $change field.
* @param patch the patch class instance.
* @return the previous patch instance.
*/
private Object patchClass(Field changeField, Object patch) throws IllegalAccessException {
Object previous = changeField.get(null);
changeField.set(null, patch);
return previous;
}
反射创建更改的类对应的$override类的实例,赋给这个类的$change字段。正因如此,才能做到不重启activity实现Hot Swap !
Warm Swap
回到之前InstantRunClient的源码看:1
2
3
4
5
6
7
8
9
10
11
12case RESOURCES:
updateMode = updateMode.combine(UpdateMode.WARM_SWAP);
files.add(FileTransfer.createResourceFile(file));
break;
......
public static FileTransfer createResourceFile(@NonNull File source) {
return new FileTransfer(TRANSFER_MODE_RESOURCES, source, Paths.RESOURCE_FILE_NAME);
}
......
/** Name of file to write resource data into, if not extracting resources */
public static final String RESOURCE_FILE_NAME = "resources.ap_";
结合之前的分析可知是将patch数据写入resources.ap_然后push到了server端。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 case MESSAGE_PATCHES: {
if (!authenticate(input)) {
return;
}
List<ApplicationPatch> changes = ApplicationPatch.read(input);
if (changes == null) {
continue;
}
boolean hasResources = hasResources(changes);
int updateMode = input.readInt();
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();
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
updateMode = Math.max(updateMode, UPDATE_MODE_WARM_SWAP);
return updateMode;
}
server收到warm swap消息和patch数据后先清空原来的resource更新文件,然后写入新的resources.ap_。接着调restart方法。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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110private void restart(int updateMode, boolean incrementalResources, boolean toast) {
if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) {
Log.v(Logging.LOG_TAG, "Finished loading changes; update mode =" + updateMode);
}
if (updateMode == UPDATE_MODE_NONE || updateMode == UPDATE_MODE_HOT_SWAP) {
if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) {
Log.v(Logging.LOG_TAG, "Applying incremental code without restart");
}
if (toast) {
Activity foreground = Restarter.getForegroundActivity(context);
if (foreground != null) {
Restarter.showToast(foreground, "Applied code changes without activity " +
"restart");
} else if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) {
Log.v(Logging.LOG_TAG, "Couldn't show toast: no activity found");
}
}
return;
}
List<Activity> activities = Restarter.getActivities(context, false);
if (incrementalResources && updateMode == UPDATE_MODE_WARM_SWAP) {
// Try to just replace the resources on the fly!
File file = FileManager.getExternalResourceFile();
if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) {
Log.v(
Logging.LOG_TAG,
"About to update resource file=" + file + ", activities=" + activities);
}
if (file != null) {
String resources = file.getPath();
MonkeyPatcher.monkeyPatchExistingResources(context, resources, activities);
} else {
Log.e(Logging.LOG_TAG, "No resource file found to apply");
updateMode = UPDATE_MODE_COLD_SWAP;
}
}
Activity activity = Restarter.getForegroundActivity(context);
if (updateMode == UPDATE_MODE_WARM_SWAP) {
if (activity != null) {
if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) {
Log.v(Logging.LOG_TAG, "Restarting activity only!");
}
boolean handledRestart = false;
try {
// Allow methods to handle their own restart by implementing
// public boolean onHandleCodeChange(long flags) { .... }
// and returning true if the change was handled manually
Method method = activity.getClass().getMethod("onHandleCodeChange", Long.TYPE);
Object result = method.invoke(activity, 0L);
if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) {
Log.v(
Logging.LOG_TAG,
"Activity "
+ activity
+ " provided manual restart method; return "
+ result);
}
if (Boolean.TRUE.equals(result)) {
handledRestart = true;
if (toast) {
Restarter.showToast(activity, "Applied changes");
}
}
} catch (Throwable ignore) {
}
if (!handledRestart) {
if (toast) {
Restarter.showToast(activity, "Applied changes, restarted activity");
}
Restarter.restartActivityOnUiThread(activity);
}
return;
}
if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) {
Log.v(
Logging.LOG_TAG,
"No activity found, falling through to do a full app restart");
}
updateMode = UPDATE_MODE_COLD_SWAP;
}
if (updateMode != UPDATE_MODE_COLD_SWAP) {
if (Log.isLoggable(Logging.LOG_TAG, Log.ERROR)) {
Log.e(Logging.LOG_TAG, "Unexpected update mode: " + updateMode);
}
return;
}
if (RESTART_LOCALLY) {
if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) {
Log.v(Logging.LOG_TAG, "Performing full app restart");
}
Restarter.restartApp(context, activities, toast);
} else {
if (Log.isLoggable(Logging.LOG_TAG, Log.VERBOSE)) {
Log.v(Logging.LOG_TAG, "Waiting for app to be killed and restarted by the IDE...");
}
}
}
可知Hot Swap会给取前台activity,弹toast。Warm Swap会MonkeyPatcher.monkeyPatchExistingResources进行资源替换。然后调Restarter重启activity。我们可以来看下Restarter这个类,觉得还是挺有用的。封装了重启activity,重启app,获取前台activity等方法。
1 | /** |
继续来看下核心的MonkeyPatcher.monkeyPatchExistingResources方法。
1 | public static void monkeyPatchExistingResources(@Nullable Context context, |
主要是反射创建新的AssetManager,调用addAssetPath加入新增的resources,接着反射替换调所有 activity.getResources(),activity.getTheme(),ResourceManager的mResourceReferences这几个地方用到的AssetManager.
至此Warm Swap的主要流程也分析完了。如果Warm Swap没找到前台activity的话也会降级成Cold Swap。
Cold Swap
涉及到类结构改变的修改(例如manifest ,方法签名,新增字段,继承关系变化)等会使用Cold Swap。如今的Cold Swap不同于以往,以往是采用的split dex 作为instantrun.zip打到同一个apk包里,现在会redex和package,但是只会产生涉及到修改类的split apk,然后install-multiple执行一个partial install,重启app达到修改的效果。
我们新加一个icon试下看,你可能会误认为这是一次Warm Swap,其实由于会生成新的资源id字段,而且不属于assets,所以这是一次Cold Swap。1
2
3
401/25 15:37:45: Launching app
$ adb install-multiple -r -t -p com.baidu.swan /Users/denglin03/Desktop/projects/DemoApp/app/build/intermediates/resources/instant-run/debug/resources-debug.apk /Users/denglin03/Desktop/projects/DemoApp/app/build/intermediates/split-apk/debug/slices/slice_0.apk
Split APKs installed
$ adb shell am start -n "com.baidu.swan/com.baidu.swan.MainActivity" -a android.intent.action.MAIN -c android.intent.category.LAUNCHER
可知这次只install了resources-debug.apk 和 slice_0.apk。前者新增了我们加入的icon资源,后者新增了R$mipmap类中资源id字段。看到这里,你应该知道了,为什么不直接打成一个apk而是分出10个slice apk,因为大部分更改只需要redex和package单个slice apk,从而把redex的时间降低到原先的10%左右,达到真正的”Instant” Run。
到这里我们大体已经清楚了整个过程,但是上面hot swap里分析的对代码插桩发生在哪一步呢?很容易猜到是在transform阶段,具体插桩代码可以在gradle源码找到,详见:InstantRunTransform.java
完结,撒花🎉🎉🎉
参考: