上一篇博客我们介绍了InstantRun
的初始化逻辑,接下来我们来看下在运行时阶段,InstantRun
是如何加载修改的代码的。
上一篇博客的末尾我们介绍了InstantRun
在初始化完成后,会启动一个server。不难猜测,这个server就是在监听是否有代码更新。当用户更改代码后,AndroidStudio会将相关更新发送给server,server获取到更新后执行修复逻辑。
1 SocketServerReplyThread
server的主要实现由其内部类SocketServerReplyThread
,首先来看下其实现:
private class SocketServerReplyThread extends Thread {
private final LocalSocket mSocket;
SocketServerReplyThread(LocalSocket socket) {
this.mSocket = socket;
}
public void run() {
try {
DataInputStream input = new DataInputStream(this.mSocket.getInputStream());
DataOutputStream output = new DataOutputStream(this.mSocket.getOutputStream());
try {
handle(input, output);
} finally {
try {
input.close();
} catch (IOException ignore) {
}
try {
output.close();
} catch (IOException ignore) {
}
}
return;
} catch (IOException e) {
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Fatal error receiving messages", e);
}
}
}
private void handle(DataInputStream input, DataOutputStream output) throws IOException {
long magic = input.readLong();
if (magic != 890269988L) {
Log.w("InstantRun", "Unrecognized header format " + Long.toHexString(magic));
return;
}
int version = input.readInt();
output.writeInt(4);
if (version != 4) {
Log.w("InstantRun", "Mismatched protocol versions; app is using version 4 and tool is using version " + version);
} else {
int message;
for (; ; ) {
message = input.readInt();
switch (message) {
case 7:
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Received EOF from the IDE");
}
return;
case 2:
boolean active = Restarter.getForegroundActivity(Server.this.mApplication) != null;
output.writeBoolean(active);
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Received Ping message from the IDE; returned active = " + active);
}
break;
case 3:
String path = input.readUTF();
long size = FileManager.getFileSize(path);
output.writeLong(size);
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Received path-exists(" + path + ") from the " + "IDE; returned size=" + size);
}
break;
case 4:
long begin = System.currentTimeMillis();
path = input.readUTF();
byte[] checksum = FileManager.getCheckSum(path);
if (checksum != null) {
output.writeInt(checksum.length);
output.write(checksum);
if (Log.isLoggable("InstantRun", 2)) {
long end = System.currentTimeMillis();
String hash = new BigInteger(1, checksum)
.toString(16);
Log.v("InstantRun", "Received checksum(" + path
+ ") from the " + "IDE: took "
+ (end - begin) + "ms to compute "
+ hash);
}
} else {
output.writeInt(0);
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Received checksum(" + path
+ ") from the "
+ "IDE: returning ");
}
}
break;
case 5:
if (!authenticate(input)) {
return;
}
Activity activity = Restarter
.getForegroundActivity(Server.this.mApplication);
if (activity != null) {
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun",
"Restarting activity per user request");
}
Restarter.restartActivityOnUiThread(activity);
}
break;
case 1:
if (!authenticate(input)) {
return;
}
List changes = ApplicationPatch
.read(input);
if (changes != null) {
boolean hasResources = Server.hasResources(changes);
int updateMode = input.readInt();
updateMode = Server.this.handlePatches(changes,
hasResources, updateMode);
boolean showToast = input.readBoolean();
output.writeBoolean(true);
Server.this.restart(updateMode, hasResources,
showToast);
}
break;
case 6:
String text = input.readUTF();
Activity foreground = Restarter
.getForegroundActivity(Server.this.mApplication);
if (foreground != null) {
Restarter.showToast(foreground, text);
} else if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun",
"Couldn't show toast (no activity) : "
+ text);
}
break;
}
}
}
}
}
socket开启后,开始读取数据,先进行一些简单的校验,判断读取的数据是否正确。然后依次读取文件数据。
- 如果读到7,则表示已经读到文件的末尾,退出读取操作
- 如果读到2,则表示获取当前Activity活跃状态,并且进行记录
- 如果读到3,读取UTF-8字符串路径,读取该路径下文件长度,并且进行记录
- 如果读到4,读取UTF-8字符串路径,获取该路径下文件MD5值,如果没有,则记录0,否则记录MD5值和长度。
- 如果读到5,先校验输入的值是否正确(根据token来判断),如果正确,则在UI线程重启Activity
- 如果读到1,先校验输入的值是否正确(根据token来判断),如果正确,获取代码变化的List,处理代码的改变(handlePatches,这个之后具体分析),然后重启
- 如果读到6,读取UTF-8字符串,showToast
当读到1时,获取代码变化的ApplicationPatch列表,然后调用handlePatches来处理代码的变化。
handlePatches:
private int handlePatches(List changes, boolean hasResources, int updateMode) {
if (hasResources) {
FileManager.startUpdate();
}
for (ApplicationPatch change : changes) {
String path = change.getPath();
if (path.endsWith(".dex")) {
handleColdSwapPatch(change);
boolean canHotSwap = false;
for (ApplicationPatch c : changes) {
if (c.getPath().equals("classes.dex.3")) {
canHotSwap = true;
break;
}
}
if (!canHotSwap) {
updateMode = 3;
}
} else if (path.equals("classes.dex.3")) {
updateMode = handleHotSwapPatch(updateMode, change);
} else if (isResourcePath(path)) {
updateMode = handleResourcePatch(updateMode, change, path);
}
}
if (hasResources) {
FileManager.finishUpdate(true);
}
return updateMode;
}
本方法主要通过判断Change的内容,来判断采用什么模式(热部署、温部署或冷部署)
- 如果后缀为“.dex”,冷部署处理handleColdSwapPatch
- 如果后缀为“classes.dex.3”,热部署处理handleHotSwapPatch
- 其他情况,温部署,处理资源handleResourcePatch
2 热部署
我们知道如果仅仅修改某个方法的内部实现,InstantRun
可以通过热部署的方式更新。还是以上一篇博客的例子,我们对代码进行一点修改,将Toast弹出的文字从'click'变为'click!!!':
@Override
public void onClick(View view) {
Toast.makeText(this, "click!!!", Toast.LENGTH_SHORT).show();
}
此时如果点击运行,可以看到应用在没有重启的情况更新了逻辑。当点击run按钮后,在build/intermediates/transforms/instantRun/debug/folders/4000/5目录下会出现我们输出即将发送给终端的patch:
可以看到patch总共分为两部分:
- 修改后的代码,对应图中的
com.alibaba.sdk.instantdemo.MainActivity$override
-
com.android.tools.fd.runtime.AppPatchesLoaderImpl.class
用于记录哪些类被修改了,如本例中的MainActivity
public class AppPatchesLoaderImpl extends AbstractPatchesLoaderImpl { public static final long BUILD_ID = 76160209775610L; public AppPatchesLoaderImpl() { } public String[] getPatchedClasses() { return new String[]{"com.alibaba.sdk.instandemo.MainActivity"}; } }
2.1 修改后的代码
修改后的代码会重新生成一个新的类名:旧类名+$override
。如本例中的MainActivity$override
,接下来看下MainActivity$override
的源码:
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(2130968603);
AndroidInstantRuntime.setPrivateField($this, (Button)$this.findViewById(2131427416), MainActivity.class, "btn");
((Button)AndroidInstantRuntime.getPrivateField($this, MainActivity.class, "btn")).setOnClickListener($this);
}
public static void onClick(MainActivity $this, View view) {
Toast.makeText($this, "click!!!", 0).show();
}
public Object access$dispatch(String var1, Object... var2) {
switch(var1.hashCode()) {
case -1912803358:
onClick((MainActivity)var2[0], (View)var2[1]);
return null;
case -641568046:
onCreate((MainActivity)var2[0], (Bundle)var2[1]);
return null;
case 1345615064:
init$body((MainActivity)var2[0], (Object[])var2[1]);
return null;
case 1495908858:
return init$args((MainActivity[])var2[0], (Object[])var2[1]);
default:
throw new InstantReloadException(String.format("String switch could not find \'%s\' with hashcode %s in %s", new Object[]{var1, Integer.valueOf(var1.hashCode()), "com/alibaba/sdk/instandemo/MainActivity"}));
}
}
}
我们看到,MainActivity$override
实现了IncrementalChange
并覆写了access$dispatch
方法。
该patch会通过server被写到应用的私有目录下,然后通过handleHotSwapPatch进行加载。
2.2 hot swap:handleHotSwapPatch
private int handleHotSwapPatch(int updateMode, ApplicationPatch patch) {
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Received incremental code patch");
}
try {
String dexFile = FileManager.writeTempDexFile(patch.getBytes());
if (dexFile == null) {
Log.e("InstantRun", "No file to write the code to");
return updateMode;
}
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Reading live code from " + dexFile);
}
String nativeLibraryPath = FileManager.getNativeLibraryFolder()
.getPath();
DexClassLoader dexClassLoader = new DexClassLoader(dexFile,
this.mApplication.getCacheDir().getPath(),
nativeLibraryPath, getClass().getClassLoader());
Class aClass = Class.forName(
"com.android.tools.fd.runtime.AppPatchesLoaderImpl", true,
dexClassLoader);
try {
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Got the patcher class " + aClass);
}
PatchesLoader loader = (PatchesLoader) aClass.newInstance();
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Got the patcher instance " + loader);
}
String[] getPatchedClasses = (String[]) aClass
.getDeclaredMethod("getPatchedClasses", new Class[0])
.invoke(loader, new Object[0]);
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Got the list of classes ");
for (String getPatchedClass : getPatchedClasses) {
Log.v("InstantRun", "class " + getPatchedClass);
}
}
if (!loader.load()) {
updateMode = 3;
}
} catch (Exception e) {
Log.e("InstantRun", "Couldn't apply code changes", e);
e.printStackTrace();
updateMode = 3;
}
} catch (Throwable e) {
Log.e("InstantRun", "Couldn't apply code changes", e);
updateMode = 3;
}
return updateMode;
}
该方法将patch的dex文件写入到临时目录,然后使用DexClassLoader去加载dex。然后反射调用AppPatchesLoaderImpl类的load方法。
AppPatchesLoaderImpl继承自抽象类AbstractPatchesLoaderImpl,并实现了抽象方法:getPatchedClasses。而AbstractPatchesLoaderImpl抽象类代码如下:
public abstract class AbstractPatchesLoaderImpl implements PatchesLoader {
public abstract String[] getPatchedClasses();
public boolean load() {
try {
for (String className : getPatchedClasses()) {
ClassLoader cl = getClass().getClassLoader();
Class aClass = cl.loadClass(className + "$override");
Object o = aClass.newInstance();
Class originalClass = cl.loadClass(className);
Field changeField = originalClass.getDeclaredField("$change");
changeField.setAccessible(true);
Object previous = changeField.get(null);
if (previous != null) {
Field isObsolete = previous.getClass().getDeclaredField("$obsolete");
if (isObsolete != null) {
isObsolete.set(null, Boolean.valueOf(true));
}
}
changeField.set(null, o);
if ((Log.logging != null) && (Log.logging.isLoggable(Level.FINE))) {
Log.logging.log(Level.FINE, String.format("patched %s", new Object[] { className }));
}
}
} catch (Exception e) {
if (Log.logging != null) {
Log.logging.log(Level.SEVERE, String.format("Exception while patching %s", new Object[] { "foo.bar" }), e);
}
return false;
}
return true;
}
}
现在我们再回过头去看下MainActivity
的代码:
package com.alibaba.sdk.instandemo;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.Toast;
import com.android.tools.fd.runtime.IncrementalChange;
import com.android.tools.fd.runtime.InstantReloadException;
public class MainActivity extends AppCompatActivity
implements View.OnClickListener
{
public static final long serialVersionUID = 0L;
private Button btn;
public MainActivity()
{
}
MainActivity(Object[] paramArrayOfObject, InstantReloadException paramInstantReloadException)
{
this();
}
public void onClick(View paramView)
{
IncrementalChange localIncrementalChange = $change;
if (localIncrementalChange != null)
{
localIncrementalChange.access$dispatch("onClick.(Landroid/view/View;)V", new Object[] { this, paramView });
return;
}
Toast.makeText(this, "click", 0).show();
}
public void onCreate(Bundle paramBundle)
{
IncrementalChange localIncrementalChange = $change;
if (localIncrementalChange != null)
{
localIncrementalChange.access$dispatch("onCreate.(Landroid/os/Bundle;)V", new Object[] { this, paramBundle });
return;
}
super.onCreate(paramBundle);
setContentView(2130968603);
this.btn = ((Button)findViewById(2131427416));
this.btn.setOnClickListener(this);
}
}
结合两段代码,不难看出,loadClass方法的原理其实就是通过反射的方法将原有class中的$change
设置为修复类,然后通过access$dispatch
执行更新后的逻辑。
这里有一个问题。如果我多次修改MainActivity
,handleHotSwapPatch
就会加载多次MainActivity$override
,难道不会冲突吗?一个类不是只能加载一次吗?其实这个不用担心,因为handleHotSwapPatch
每次都重新创建了一个DexClassLoader
,不同的ClassLoader即使加载同一个class也会被认为是不同class,所以不用担心。
2.3 warm swap:handleResourcePatch
private static int handleResourcePatch(int updateMode, ApplicationPatch patch, String path){
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Received resource changes (" + path + ")");
}
FileManager.writeAaptResources(path, patch.getBytes());
updateMode = Math.max(updateMode, 2);
return updateMode;
}
调用了FileManager.writeAaptResources方法写入Aapt resource。
public static void writeAaptResources(String relativePath, byte[] bytes){
File resourceFile = getResourceFile(getWriteFolder(false));
File file = resourceFile;
File folder = file.getParentFile();
if (!folder.isDirectory()) {
boolean created = folder.mkdirs();
if (!created) {
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Cannot create local resource file directory " + folder);
}
return;
}
}
if (relativePath.equals("resources.ap_"))
{
writeRawBytes(file, bytes);
}
else
writeRawBytes(file, bytes);
}
可以看到它去获取了对应的资源文件,就是我们在上面提到的/data/data/[applicationId]/files/instant-run/resources.ap_,InstantRun直接对它进行了字节码操作,把通过Socket传过来的修改过的资源传递了进去。对Android上的资源打包不了解的同学可以去看老罗的Android应用程序资源的编译和打包过程分析这篇文章。
2.4 cold swap:handleColdSwapPatch
private static void handleColdSwapPatch(ApplicationPatch patch) {
if (patch.path.startsWith("slice-")) {
File file = FileManager.writeDexShard(patch.getBytes(), patch.path);
if (Log.isLoggable("InstantRun", 2))
Log.v("InstantRun", "Received dex shard " + file);
}
}
public static File writeDexShard(byte[] bytes, String name){
File dexFolder = getDexFileFolder(getDataFolder(), true);
if (dexFolder == null) {
return null;
}
File file = new File(dexFolder, name);
writeRawBytes(file, bytes);
return file;
}
对于cold swap,其实就是把数据写进对应的dex中,所以在art的情况下需要重启app,而对于API20以下的只能重新构建和部署了。
3 总结
两篇博客大致介绍了InstantRun
的原理,从宏观上讲,InstantRun
通过创建宿主Application的方式来代理所有来的加载,为热更新提供了Runtime。从微观上来讲,三种情况的原理各有不同:
- hot swap玩的方法替换,通过重新生成一个新类,并将原有类的方法映射到新类中的方法。思想上比较类似AndFix,不过AndFix的更新在native层完成,hot swap则是在java层通过插桩完成。不熟悉AndFix的朋友可以看下这篇博客:AndFix Bug热修复框架及源码解析
- warm swap的原理是加载
resources.ap_
并写入到AssetManager的加载路径中 - cold swap的原理其实就是把数据写进对应的dex中。