InstantRun原理以及源码分析

前言

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.
*/
@Documented
@Retention(RetentionPolicy.CLASS)
@Target({PACKAGE, TYPE, CONSTRUCTOR, METHOD})
public @interface DisableInstantRun {
}

我们点Android studio run这个按钮右边的⚡️就可以使用InstantRun应用最新的更改,前提是在Preferences里开了enable InstantRun。我们来看下,第一次点run发生了什么:

1
2
3
4
5
6
7
8
adb 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
59
public 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
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
public class MainActivity$override implements IncrementalChange {
public MainActivity$override() {
}

public static Object init$args(MainActivity[] var0, Object[] var1) {
Object[] var2 = new Object[]{new Object[]{var0, new Object[0]}, "android/support/v7/app/AppCompatActivity.()V"};
return var2;
}

public static void init$body(MainActivity $this, Object[] var1) {
}

public static void onCreate(MainActivity $this, Bundle savedInstanceState) {
Object[] var2 = new Object[]{savedInstanceState};
MainActivity.access$super($this, "onCreate.(Landroid/os/Bundle;)V", var2);
$this.setContentView(2131296284);
Log.d("tag", "instant run patch ----");
}

public static void onResume(MainActivity $this) {
Object[] var1 = new Object[0];
MainActivity.access$super($this, "onResume.()V", var1);
$this.startAction();
}

public static void startAction(MainActivity $this) {
Object[] var1 = new Object[0];
Class[] var10001 = new Class[0];
String var10002 = "<init>";
((DemoAction)((DemoAction)AndroidInstantRuntime.newForClass(var1, var10001, DemoAction.class))).start();
}

public Object access$dispatch(String var1, Object... var2) {
switch(var1.hashCode()) {
case -1512649357:
onResume((MainActivity)var2[0]);
return null;
case -1058486495:
return init$args((MainActivity[])var2[0], (Object[])var2[1]);
case -776359727:
init$body((MainActivity)var2[0], (Object[])var2[1]);
return null;
case -641568046:
onCreate((MainActivity)var2[0], (Bundle)var2[1]);
return null;
case -544012865:
startAction((MainActivity)var2[0]);
return null;
default:
throw new InstantReloadException(String.format("String switch could not find '%s' with hashcode %s in %s", var1, var1.hashCode(), "com/baidu/swan/MainActivity"));
}
}
}

可以知道对MainActivity任意方法的调用会代理到MainActivity$override的access$dispatch方法,传入一个类似方法签名的字符串,access$dispatch根据这个字符串的hashcode分发到MainActivity$override对应的方法中。

下面来看下IR源码,从client的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
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
public UpdateMode pushPatches(@NonNull IDevice device,
@NonNull final InstantRunBuildInfo buildInfo,
@NonNull UpdateMode updateMode,
final boolean isRestartActivity,
final boolean isShowToastEnabled) throws InstantRunPushFailedException, IOException {
if (!buildInfo.canHotswap()) {
updateMode = updateMode.combine(UpdateMode.COLD_SWAP);
}

List<FileTransfer> files = Lists.newArrayList();

boolean appInForeground;
boolean appRunning;
try {
AppState appState = getAppState(device);
appInForeground = appState == AppState.FOREGROUND;
appRunning = appState == AppState.FOREGROUND || appState == AppState.BACKGROUND;
} catch (IOException e) {
appInForeground = appRunning = false;
}

List<InstantRunArtifact> artifacts = buildInfo.getArtifacts();
mLogger.info("Artifacts from build-info.xml: " + Joiner.on("-").join(artifacts));
for (InstantRunArtifact artifact : artifacts) {
InstantRunArtifactType type = artifact.type;
File file = artifact.file;
switch (type) {
case MAIN:
// Should never be used with this method: APKs should be pushed by DeployApkTask
assert false : artifact;
break;
case SPLIT_MAIN:
// Should only be used here when we're doing a *compatible*
// resource swap and also got an APK for split. Ignore here.
continue;
case SPLIT:
// we expect SPLIT APK for resource changes on O and above
continue;
case RESOURCES:
updateMode = updateMode.combine(UpdateMode.WARM_SWAP);
files.add(FileTransfer.createResourceFile(file));
break;
case RELOAD_DEX:
if (appInForeground) {
files.add(FileTransfer.createHotswapPatch(file));
} else {
// Gradle created a reload dex, but the app is no longer running.
// If it created a cold swap artifact, we can use it; otherwise we're out of luck.
if (!buildInfo.hasOneOf(SPLIT)) {
throw new InstantRunPushFailedException(
"Can't apply hot swap patch: app is no longer running");
}
}
break;
default:
assert false : artifact;
}
}

boolean needRestart;

if (appRunning) {
List<ApplicationPatch> changes = new ArrayList<>(files.size());
for (FileTransfer file : files) {
try {
changes.add(file.getPatch());
}
catch (IOException e) {
throw new InstantRunPushFailedException("Could not read file " + file);
}
}
updateMode = pushPatches(device, buildInfo.getTimeStamp(), changes,
updateMode, isRestartActivity, isShowToastEnabled);

needRestart = false;
if (!appInForeground || !buildInfo.canHotswap()) {
stopApp(device, false /* sendChangeBroadcast */);
needRestart = true;
}
}
else {
return UpdateMode.COLD_SWAP;
}

logFilesPushed(files, needRestart);

if (needRestart) {
// TODO: this should not need to be explicit, but leaving in to ensure no behaviour change.
return UpdateMode.COLD_SWAP;
}
return updateMode;
}

可知针对资源的更新采取的是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
53
public 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>() {
@Override
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;
}

@Override
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
21
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;
}

hot swap调用的是handleHotSwapPatch

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

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
10
public 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
77
public abstract String[] getPatchedClasses();

@Override
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
12
case RESOURCES:
updateMode = updateMode.combine(UpdateMode.WARM_SWAP);
files.add(FileTransfer.createResourceFile(file));
break;
......
@NonNull
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
110
private 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
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
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
/**
* Handler capable of restarting parts of the application in order for changes to become apparent to
* the user:
*
* <ul>
* <li>Apply a tiny change immediately - possible if we can detect that the change is only used in
* a limited context (such as in a layout) and we can directly poke the view hierarchy and
* schedule a paint.
* <li>Apply a change to the current activity. We can restart just the activity while the app
* continues running.
* <li>Restart the app with state persistence (simulates what happens when a user puts an app in
* the background, then it gets killed by the memory monitor, and then restored when the user
* brings it back
* <li>Restart the app completely.
* </ul>
*/
public class Restarter {
/** Restart an activity. Should preserve as much state as possible. */
public static void restartActivityOnUiThread(@NonNull final Activity activity) {
activity.runOnUiThread(
new Runnable() {
@Override
public void run() {
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Resources updated: notify activities");
}
updateActivity(activity);
}
});
}

private static void restartActivity(@NonNull Activity activity) {
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "About to restart " + activity.getClass().getSimpleName());
}

// You can't restart activities that have parents: find the top-most activity
while (activity.getParent() != null) {
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(
LOG_TAG,
activity.getClass().getSimpleName()
+ " is not a top level activity; restarting "
+ activity.getParent().getClass().getSimpleName()
+ " instead");
}
activity = activity.getParent();
}

// Directly supported by the framework!
activity.recreate();
}

/**
* Attempt to restart the app. Ideally this should also try to preserve as much state as
* possible:
* <ul>
* <li>The current activity</li>
* <li>If possible, state in the current activity, and</li>
* <li>The activity stack</li>
* </ul>
*
* This may require some framework support. Apparently it may already be possible
* (Dianne says to put the app in the background, kill it then restart it; need to
* figure out how to do this.)
*/
public static void restartApp(@Nullable Context appContext,
@NonNull Collection<Activity> knownActivities,
boolean toast) {
if (!knownActivities.isEmpty()) {
// Can't live patch resources; instead, try to restart the current activity
Activity foreground = getForegroundActivity(appContext);

if (foreground != null) {
// http://stackoverflow.com/questions/6609414/howto-programatically-restart-android-app
//noinspection UnnecessaryLocalVariable
if (toast) {
showToast(foreground, "Restarting app to apply incompatible changes");
}
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "RESTARTING APP");
}
@SuppressWarnings("UnnecessaryLocalVariable") // fore code clarify
Context context = foreground;
Intent intent = new Intent(context, foreground.getClass());
int intentId = 0;
PendingIntent pendingIntent = PendingIntent.getActivity(context, intentId,
intent, PendingIntent.FLAG_CANCEL_CURRENT);
AlarmManager mgr = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, pendingIntent);
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(
LOG_TAG,
"Scheduling activity "
+ foreground
+ " to start after exiting process");
}
} else {
showToast(knownActivities.iterator().next(), "Unable to restart app");
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(
LOG_TAG,
"Couldn't find any foreground activities to restart "
+ "for resource refresh");
}
}
System.exit(0);
}
}

static void showToast(@NonNull final Activity activity, @NonNull final String text) {
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "About to show toast for activity " + activity + ": " + text);
}
activity.runOnUiThread(
new Runnable() {
@Override
public void run() {
try {
Context context = activity.getApplicationContext();
if (context instanceof ContextWrapper) {
Context base = ((ContextWrapper) context).getBaseContext();
if (base == null) {
if (Log.isLoggable(LOG_TAG, Log.WARN)) {
Log.w(LOG_TAG, "Couldn't show toast: no base context");
}
return;
}
}

// For longer messages, leave the message up longer
int duration = Toast.LENGTH_SHORT;
if (text.length() >= 60 || text.indexOf('\n') != -1) {
duration = Toast.LENGTH_LONG;
}

// Avoid crashing when not available, e.g.
// java.lang.RuntimeException: Can't create handler inside thread that has
// not called Looper.prepare()
Toast.makeText(activity, text, duration).show();
} catch (Throwable e) {
if (Log.isLoggable(LOG_TAG, Log.WARN)) {
Log.w(LOG_TAG, "Couldn't show toast", e);
}
}
}
});
}

@Nullable
public static Activity getForegroundActivity(@Nullable Context context) {
List<Activity> list = getActivities(context, true);
return list.isEmpty() ? null : list.get(0);
}

// http://stackoverflow.com/questions/11411395/how-to-get-current-foreground-activity-context-in-android
@NonNull
public static List<Activity> getActivities(@Nullable Context context, boolean foregroundOnly) {
List<Activity> list = new ArrayList<Activity>();
try {
Class activityThreadClass = Class.forName("android.app.ActivityThread");
Object activityThread = MonkeyPatcher.getActivityThread(context, activityThreadClass);
Field activitiesField = activityThreadClass.getDeclaredField("mActivities");
activitiesField.setAccessible(true);

// check app hasn't crashed, if it has, return empty list of activities.
if (hasAppCrashed(context, activityThreadClass, activityThread)) {
return new ArrayList<Activity>();
}

Collection c;
Object collection = activitiesField.get(activityThread);

if (collection instanceof HashMap) {
// Older platforms
Map activities = (HashMap) collection;
c = activities.values();
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT &&
collection instanceof ArrayMap) {
ArrayMap activities = (ArrayMap) collection;
c = activities.values();
} else {
return list;
}

for (Object activityClientRecord : c) {
Class activityClientRecordClass = activityClientRecord.getClass();
if (foregroundOnly) {
Field pausedField = activityClientRecordClass.getDeclaredField("paused");
pausedField.setAccessible(true);
if (pausedField.getBoolean(activityClientRecord)) {
continue;
}
}
Field activityField = activityClientRecordClass.getDeclaredField("activity");
activityField.setAccessible(true);
Activity activity = (Activity) activityField.get(activityClientRecord);
if (activity != null) {
list.add(activity);
}
}
} catch (Throwable e) {
if (Log.isLoggable(LOG_TAG, Log.WARN)) {
Log.w(LOG_TAG, "Error retrieving activities", e);
}
}
return list;
}

/**
* Checks if the application has crashed by comparing the package name against the list of
* processes in error state.
*/
private static boolean hasAppCrashed(
@Nullable Context context,
@NonNull Class activityThreadClass,
@Nullable Object activityThread)
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
if (context == null || activityThread == null) {
return false;
}

String currentPackageName = getPackageName(activityThreadClass, activityThread);

ActivityManager manager =
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.ProcessErrorStateInfo> processesInErrorState =
manager.getProcessesInErrorState();
if (processesInErrorState != null) { // returns null if no process in error state
for (ActivityManager.ProcessErrorStateInfo info : processesInErrorState) {
if (info.processName.equals(currentPackageName) && info.condition != NO_ERROR) {
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "App Thread has crashed, return empty activity list.");
}
return true;
}
}
}
return false;
}

// Use reflection to determine the package name from activity thread.
private static String getPackageName(
@NonNull Class activityThreadClass, @Nullable Object activityThread)
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
Method currentPackageNameMethod =
activityThreadClass.getDeclaredMethod("currentPackageName");
return (String) currentPackageNameMethod.invoke(activityThread);
}

private static void updateActivity(@NonNull Activity activity) {
// This method can be called for activities that are not in the foreground, as long
// as some of its resources have been updated. Therefore we'll need to make sure
// that this activity is in the foreground, and if not do nothing. Ways to do
// that are outlined here:
// http://stackoverflow.com/questions/3667022/checking-if-an-android-application-is-running-in-the-background/5862048#5862048

// Try to force re-layout; there are many approaches; see
// http://stackoverflow.com/questions/5991968/how-to-force-an-entire-layout-view-refresh

// This doesn't seem to update themes properly -- may need to do recreate() instead!
//getWindow().getDecorView().findViewById(android.R.id.content).invalidate();

// This is a bit of a sledgehammer. We should consider having an incremental updater,
// similar to IntelliJ's Look &amp; Feel updater which iterates to the view hierarchy
// and tries to incrementally refresh the LAF delegates and force a repaint.
// On the other hand, we may never be able to succeed with that, since there could be
// UI elements on the screen cached from callbacks. I should probably *not* attempt
// to try to poke the user's data models; recreating the current layout should be
// enough (e.g. if a layout references @string/foo, we'll recreate those widgets
// if (mLastContentView != -1) {
// setContentView(mLastContentView);
// } else {
// recreate();
// }
// -- nope, even that's iffy. I had code which *after* calling setContentView would
// do some findViewById calls etc to reinitialize views.
//
// So what I should really try to do is have some knowledge about what changed,
// and see if I can figure out that the change is minor (e.g. doesn't affect themes
// or layout parameters etc), and if so, just try to poke the view hierarchy directly,
// and if not, just recreate

// if (changeManager.isSimpleDelta()) {
// changeManager.applyDirectly(this);
// } else {

// Note: This doesn't handle manifest changes like changing the application title

restartActivity(activity);
}
}

继续来看下核心的MonkeyPatcher.monkeyPatchExistingResources方法。

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
public static void monkeyPatchExistingResources(@Nullable Context context,
@Nullable String externalResourceFile,
@Nullable Collection<Activity> activities) {
if (externalResourceFile == null) {
return;
}

try {
// Create a new AssetManager instance and point it to the resources installed under
// /sdcard
AssetManager newAssetManager = AssetManager.class.getConstructor().newInstance();
Method mAddAssetPath = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
mAddAssetPath.setAccessible(true);
if (((Integer) mAddAssetPath.invoke(newAssetManager, externalResourceFile)) == 0) {
throw new IllegalStateException("Could not create new AssetManager");
}

// Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm
// in L, so we do it unconditionally.
Method mEnsureStringBlocks = AssetManager.class.getDeclaredMethod("ensureStringBlocks");
mEnsureStringBlocks.setAccessible(true);
mEnsureStringBlocks.invoke(newAssetManager);

if (activities != null) {
for (Activity activity : activities) {
Resources resources = activity.getResources();

try {
Field mAssets = Resources.class.getDeclaredField("mAssets");
mAssets.setAccessible(true);
mAssets.set(resources, newAssetManager);
} catch (Throwable ignore) {
Field mResourcesImpl = Resources.class.getDeclaredField("mResourcesImpl");
mResourcesImpl.setAccessible(true);
Object resourceImpl = mResourcesImpl.get(resources);
Field implAssets = resourceImpl.getClass().getDeclaredField("mAssets");
implAssets.setAccessible(true);
implAssets.set(resourceImpl, newAssetManager);
}

Resources.Theme theme = activity.getTheme();
try {
try {
Field ma = Resources.Theme.class.getDeclaredField("mAssets");
ma.setAccessible(true);
ma.set(theme, newAssetManager);
} catch (NoSuchFieldException ignore) {
Field themeField = Resources.Theme.class.getDeclaredField("mThemeImpl");
themeField.setAccessible(true);
Object impl = themeField.get(theme);
Field ma = impl.getClass().getDeclaredField("mAssets");
ma.setAccessible(true);
ma.set(impl, newAssetManager);
}

Field mt = ContextThemeWrapper.class.getDeclaredField("mTheme");
mt.setAccessible(true);
mt.set(activity, null);
Method mtm = ContextThemeWrapper.class.getDeclaredMethod("initializeTheme");
mtm.setAccessible(true);
mtm.invoke(activity);

if (SDK_INT < 24) { // As of API 24, mTheme is gone (but updates work
// without these changes
Method mCreateTheme = AssetManager.class
.getDeclaredMethod("createTheme");
mCreateTheme.setAccessible(true);
Object internalTheme = mCreateTheme.invoke(newAssetManager);
Field mTheme = Resources.Theme.class.getDeclaredField("mTheme");
mTheme.setAccessible(true);
mTheme.set(theme, internalTheme);
}
} catch (Throwable e) {
Log.e(LOG_TAG, "Failed to update existing theme for activity " + activity,
e);
}

pruneResourceCaches(resources);
}
}

// Iterate over all known Resources objects
Collection<WeakReference<Resources>> references;
if (SDK_INT >= KITKAT) {
// Find the singleton instance of ResourcesManager
Class<?> resourcesManagerClass = Class.forName("android.app.ResourcesManager");
Method mGetInstance = resourcesManagerClass.getDeclaredMethod("getInstance");
mGetInstance.setAccessible(true);
Object resourcesManager = mGetInstance.invoke(null);
try {
Field fMActiveResources = resourcesManagerClass.getDeclaredField("mActiveResources");
fMActiveResources.setAccessible(true);
@SuppressWarnings("unchecked")
ArrayMap<?, WeakReference<Resources>> arrayMap =
(ArrayMap<?, WeakReference<Resources>>) fMActiveResources.get(resourcesManager);
references = arrayMap.values();
} catch (NoSuchFieldException ignore) {
Field mResourceReferences = resourcesManagerClass.getDeclaredField("mResourceReferences");
mResourceReferences.setAccessible(true);
//noinspection unchecked
references = (Collection<WeakReference<Resources>>) mResourceReferences.get(resourcesManager);
}
} else {
Class<?> activityThread = Class.forName("android.app.ActivityThread");
Field fMActiveResources = activityThread.getDeclaredField("mActiveResources");
fMActiveResources.setAccessible(true);
Object thread = getActivityThread(context, activityThread);
@SuppressWarnings("unchecked")
HashMap<?, WeakReference<Resources>> map =
(HashMap<?, WeakReference<Resources>>) fMActiveResources.get(thread);
references = map.values();
}
for (WeakReference<Resources> wr : references) {
Resources resources = wr.get();
if (resources != null) {
// Set the AssetManager of the Resources instance to our brand new one
try {
Field mAssets = Resources.class.getDeclaredField("mAssets");
mAssets.setAccessible(true);
mAssets.set(resources, newAssetManager);
} catch (Throwable ignore) {
Field mResourcesImpl = Resources.class.getDeclaredField("mResourcesImpl");
mResourcesImpl.setAccessible(true);
Object resourceImpl = mResourcesImpl.get(resources);
Field implAssets = resourceImpl.getClass().getDeclaredField("mAssets");
implAssets.setAccessible(true);
implAssets.set(resourceImpl, newAssetManager);
}

resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics());
}
}
} catch (Throwable e) {
throw new IllegalStateException(e);
}
}

主要是反射创建新的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
4
01/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

完结,撒花🎉🎉🎉

参考: