Instant Run的原理

前言

最近在学习热修复的相关知识,阿里热修复框架的存在哪些问题,这是因为Android P中对私有API的封锁导致的问题。

1
Android P引入了针对非SDK接口的使用限制,无论是直接使用还是通过反射或JNI间接使用。保留非 SDK接口的后果:在后续版本的Developer Preview中,各种访问非SDK接口的方式都会产生错误或者其他不希望的后果。

国内绝大部分的Android插件化及热修复开源库都或多或少的使用了非SDK接口,而且核心实现也都依赖着这些非SDK接口。
虽然Google官方在文档中也说了,会通过提供浅灰名单的方式,开放部分非SDK接口的调用:

1
浅灰名单包含在Android P中继续工作,但我们不能保证在未来版本的平台中能够继续访问的函数和字段。 如由于某种原因,您不能实现替代列入浅灰名单的 API 的方案,则可以提交错误,以请求重新考虑此限制。

但是这并不能保证在未来版本的平台中能够继续访问这些函数和字段。所以还是需要寻找一种完全使用SDK接口的方式来实现热修复的方案。

美团点评的Robust,这个框架的实现原理参考了Google官方的InstantRun,做到了没有调用任何非SDK接口实现代码的热修复。之前虽然知道Instant Run,但是一直都没有仔细研究过他的原理,这次正好深入的研究下Instant Run的原理。

关于Instant Run的使用,建议阅读:Instant Run的使用

注:后续内容,摘自文末的参考链接。

探索

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MainActivity extends AppCompatActivity {
private Button mBtnTest;
private int mNum;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mBtnTest = (Button) findViewById(R.id.btn_test);
setListener();
}

private void setListener() {
mBtnTest.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mNum++;
Log.e("InstantRun", "Num: " + mNum);
});
}
}

点一下按钮,打出如下log:

1
08-11 16:51:16.730 4125-4125/com.wangxiandeng.instantruntest E/InstantRun: Num: 1

再点一下:

1
08-11 16:54:36.300 4125-4125/com.wangxiandeng.instantruntest E/InstantRun: Num: 2

把代码修改为:

1
Log.e("InstantRun", "Num: " + mNum*2);

点击Instan Run 闪电按钮⚡,Activty没有重启,这时候再点击按钮

1
08-11 17:02:15.340 14022-14022/com.wangxiandeng.instantruntest E/InstantRun: Num: 6

可见,mNum的值在Hot Swap时并没有重置,而是保持了之前的值:2,也就是说,Activity的所有生命周期方法并没有重走一遍,但是现在log打印出来为6,所以代码确实被替换了。

原理

Hot Swap看起来很高大上,其实玩的就是狸猫换太子的把戏。在app的第一次编译阶段,它利用transform在我们的每一个类里注入了一个变量:$change,这是一个IncrementalChange类型的变量。

证明它被注入了$change字段,现在修改onClick中的代码如下:

1
2
3
4
5
6
7
8
9
Class clazz = MainActivity.class;
try {
Field changeField = clazz.getDeclaredField("$change");
changeField.setAccessible(true);
Object changeValue = changeField.get(this);
Class changeClass = changeValue.getClass();
} catch (Exception e) {
e.printStackTrace();
}

再次点击Activity中按钮,log打印为:

1
08-11 17:25:15.830 3311-3311/com.wangxiandeng.instantruntest E/InstantRun: Class: class com.wangxiandeng.instantruntest.MainActivity$override 

事实证明,Activity中确实有$change这个变量,细心的读者还会发现,这个变量的运行类型为:

1
com.wangxiandeng.instantruntest.MainActivity$override

这里的MainActivity$override,其实就是狸猫,也就是我们经常说的补丁,它实现了IncrementalChange接口,并且重写了MainActivity中的所有方法。我们在onClick中再加一句代码:

1
printMethods(changeClass);

printMethods会打印出MainActivity$override中的所有方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void printMethods(Class cl) {
Method[] methods = cl.getDeclaredMethods();
for (Method m : methods) {
Class retType = m.getReturnType();
String name = m.getName();
System.out.print(" ");
String modifiers = Modifier.toString(m.getModifiers());
if (modifiers.length() > 0) {
System.out.print(modifiers + " ");
}
System.out.print(retType.getName() + " " + name + "(");
Class[] paramTypes = m.getParameterTypes();
for (int j = 0; j < paramTypes.length; j++) {
System.out.print(paramTypes[j].getName());
if (j < paramTypes.length - 1) {
System.out.print(", ");
}
}
System.out.println(");");
}
}

点击Activity中按钮,打印出方法如下:

1
2
3
4
5
6
7
8
9
10
11
public transient java.lang.Object access$dispatch(java.lang.String, [Ljava.lang.Object;);

public static java.lang.Object init$args([Lcom.wangxiandeng.instantruntest.MainActivity;, [Ljava.lang.Object;);

public static void init$body(com.wangxiandeng.instantruntest.MainActivity, [Ljava.lang.Object;);

public static void onCreate(com.wangxiandeng.instantruntest.MainActivity, android.os.Bundle);

public static void printMethods(java.lang.Class);

public static void setListener(com.wangxiandeng.instantruntest.MainActivity);

从Log中可以看出,MainActivityo$verride中包含了MainActivity中所有的方法,包括onCreate(), printMethods(), setListener()。

看到这里,聪明的读者应该已经猜测出Instan Run的原理了,其实也就是和代理差不多,MainActivity在执行方法时,会先判断它的代理(&change)是否为空,如果不为空,就执行代理里的方法。这样当我们修改了某个类方法里的代码,AS会自动的创建一个该类的代理(xx&override),并将代理赋值给该类的$change字段,这样我们的修改在不重启Activity的情况下也能生效了。

代理类是通过access$dispatch()方法来进行函数分发的,传入的参数为所要执行方法的签名和参数,access&dispatch()会根据方法签名的hashcode寻找到目标方法,并传入参数执行。接下来我们再来试验一下。

在MainActivity中再添加一个方法:

1
2
3
private void sayHello(String text) {
Log.e("InstantRun", text);
}

接着在onClick try块中再添加两行代码,通过反射MainActivity$override 中的access$dispatch()方法,实现调用补丁中的sayHello()。

1
2
Method dispatchMethod = changeClass.getDeclaredMethod("access$dispatch", new Class[]{String.class, Object[].class});
dispatchMethod.invoke(changeValue, "sayHello.(Ljava/lang/String;)V", new Object[]{MainActivity.this, "Hello World!"});

在第二行代码中,我们将sayHello()的方法签名以及一个“Hello World!”字符串传入给access$dispatch方法,接下来看看能不能成功的调用sayHello()。

1
08-11 17:25:15.840 3311-3311/com.wangxiandeng.instantruntest E/InstantRun: Hello World!

Log中成功的打印出了Hello World!

到这里,大家应该对Instan Run Hot Swap的来龙去脉有所了解了,那么补丁文件又是怎么加载进来的呢?当我们修改代码,并点击运行按钮时,AS会创建一个AppPatchesLoaderImpl,该类中记录了哪些类被修改了,然后通过scoket,将补丁文件和AppPatchesLoaderImpl发送到设备,调用设备的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
private int handleHotSwapPatch(int updateMode, @NonNull ApplicationPatch patch) {
try {
String dexFile = FileManager.writeTempDexFile(patch.getBytes());
String nativeLibraryPath = FileManager.getNativeLibraryFolder().getPath();
DexClassLoader dexClassLoader = new DexClassLoader(dexFile,
mApplication.getCacheDir().getPath(), nativeLibraryPath,
getClass().getClassLoader());
// we should transform this process with an interface/impl
Class<?> aClass = Class.forName(
"com.android.tools.fd.runtime.AppPatchesLoaderImpl", true, dexClassLoader);
try {
PatchesLoader loader = (PatchesLoader) aClass.newInstance();
String[] getPatchedClasses = (String[]) aClass
.getDeclaredMethod("getPatchedClasses").invoke(loader);
if (!loader.load()) {
updateMode = UPDATE_MODE_COLD_SWAP;
}
} catch (Exception e) {
updateMode = UPDATE_MODE_COLD_SWAP;
}
} catch (Throwable e) {
updateMode = UPDATE_MODE_COLD_SWAP;
}
return updateMode;
}

该方法首先新建了一个ClassLoader,将补丁记录类AppPatchesLoaderImpl加载进来,然后调用AppPatchesLoaderImpl的load方法,load()方法中会遍历并记载所有的补丁类,并反射原有类的$change变量,赋值以补丁类。

相关插件和库

Instant Run的实现依赖于一个插件和一个库,gradle plugin 2.0.0-alpha1和instant-run.jar。

gradle plugin 2.0.0-alpha1

gradle plugin 2.0.0-alpha1主要有两个作用:

  • 第一次运行,应用transform API修改字节码。

输出目录为
Application/build/intermediates/transforms/instantRun/debug/folders/1

  • 给所有的类添加**$change**字段

$changeIncrementalChange类型,IncrementalChange是个接口,该接口后面会讲。

  • 修改类的全部方法

新的逻辑是:如果**&change不为空,去调用&changeaccess$dispatch**方法,参数为方法签名字符串和方法参数数组,否则调用原逻辑。

  • 后续运行,dx补丁类,生成补丁dex。

输出目录为
Application/build/intermediates/transforms/instantRun/debug/folders/4000

  • 被修改类对应的补丁类

补丁类,并不是你修改后的类,而是由gradle plugin自动生成,实现了IncrementalChange接口的类。

该类类名在原名后面添加**&override**,复制修改后类的大部分方法,实现IncrementalChange 接口的access$dispatch方法,该方法会根据传递过来的方法签名,调用本类的同名方法。

只要把原类的**&change字段设置为该类,那就会调用该类的access$dispatch**方法,就会使用修改后的方法了。

  • 被修改类的记录类

AppPatchesLoaderImpl记录了所有被修改的类,也会被打进补丁dex。

instant-run.jar

instant-run.jar的路径为
Application/build/intermediates/incremental-runtime-classes/debug/instant-run.jar

instant-run.jar是gradle plugin帮我们自动打到dex中去的,省去了compile dependency这一步。它的主要作用有两个:

  • 设置原类的**$change**字段为补丁类

需要对被修改的类设置**$change**字段,那怎么知道哪些类被修改了?

AppPatchesLoaderImpl类不但记录了全部被修改的类,还提供load方法支持设置被修改原类**&change字段,当收到补丁通知时,只需新建一个DexClassLoader,去反射加载补丁dex中的AppPatchesLoaderImpl**类,调用load方法即可,load方法中会去加载全部补丁类,并赋值给对应原类的$change。

  • 重启加载补丁类

重启后怎么办?原来的补丁文件需要加载进来。

IncrementalClassLoader会在Application中去加载该应用cache目录中的补丁dex,把它设置为默认PathClassLoader的parent,由于ClassLoader采用双亲委托模型,会先去parent查找类,所以就可以加载补丁类了。

参考链接:

浅谈Instan Run中的热替换

从Instant run谈Android替换Application和动态加载机制

Instant Run 浅析

零私有api调用,实现Android热修复

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器