在记录自己对Instant Run的学习之前,先讲一下自己会去看Instant Run的原因吧。前一段时间,美团的Robust热修复框架开源成功在今年的Android届吸引了一大波关注,当然,我也去Robust的开源地址 和美团官方技术博客 上看了一波。在其中,设计者提到了灵感来源于Google的Instant Run方案,这也成功勾起了我的兴趣。于是便将Instant Run的源码clone了下来对其中重要的部分好好学习了一番,果然还是颇有收获。
Instant Run安装原理 Instant Run是Google在2016年的Google I/O大会上在退出Android Studio2.2版本的同时推出的其自带的一套快速部署机制,这些大家都知道了。但是它在我们写完代码按下部署按钮之后到底干了些什么呢? 在我看来,Instant Run与其说是一个运行机制,不如说就是一个Android应用程序。通过对经过Instant Run安装到手机中的应用进行反编译,我们不难发现其实安装在我们手机里的实际上并不是我们自己写的应用。实际上Instant Run是一个宿主程序,将我们自己写的程序作为资源加载进来通过对资源的解析最终将我们的程序显示出来。仔细想想,这不就是一个“双开”程序吗???当然,确实如此,不过这里我先不谈这个,以此为引子来讲一下它所带来的对于热修复的一个新的提示——快速部署。
简单的加载过程 对于加载过程的简单介绍当然是必不可少的,毕竟想要去了解Instant Run的部署功能还是要先知道它是从哪里入手的才行。 在Instant Run的宿主程序中,存在一个永远不变的主Application
——BootstrapApplication
,一切由它开始。作为一个Application
,最重要的自然是这么两个方法——onCreate()
和attachBaseContext()
。我们就先从一个应用的开始attachBaseContext()
开始看起。
BootstrapApplication
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
@Override
protected void attachBaseContext (Context context) {
if (!AppInfo.usingApkSplits) {
String apkFile = context.getApplicationInfo().sourceDir;
long apkModified = apkFile != null ? new File(apkFile).lastModified() : 0L ;
createResources(apkModified);
setupClassLoaders(context, context.getCacheDir().getPath(), apkModified);
}
createRealApplication();
super .attachBaseContext(context);
if (realApplication != null ) {
try {
Method attachBaseContext =
ContextWrapper.class.getDeclaredMethod("attachBaseContext" , Context.class);
attachBaseContext.setAccessible(true );
attachBaseContext.invoke(realApplication, context);
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
}
在这里我们看到这里所做的只是一些反射调用来做加载资源应用的一些准备工作,真正的部署工作也就落在了之后的onCreate()
方法中。
BootstrapApplication
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
@Override
public void onCreate () {
if (!AppInfo.usingApkSplits) {
MonkeyPatcher.monkeyPatchApplication(
BootstrapApplication.this , BootstrapApplication.this ,
realApplication, externalResourcePath);
MonkeyPatcher.monkeyPatchExistingResources(BootstrapApplication.this ,
externalResourcePath, null );
} else {
MonkeyPatcher.monkeyPatchApplication(
BootstrapApplication.this , BootstrapApplication.this ,
realApplication, null );
}
super .onCreate();
if (AppInfo.applicationId != null ) {
try {
boolean foundPackage = false ;
int pid = Process.myPid();
ActivityManager manager = (ActivityManager) getSystemService(
Context.ACTIVITY_SERVICE);
List<RunningAppProcessInfo> processes = manager.getRunningAppProcesses();
boolean startServer;
if (processes != null && processes.size() > 1 ) {
startServer = false ;
for (RunningAppProcessInfo processInfo : processes) {
if (AppInfo.applicationId.equals(processInfo.processName)) {
foundPackage = true ;
if (processInfo.pid == pid) {
startServer = true ;
break ;
}
}
}
if (!startServer && !foundPackage) {
startServer = true ;
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Multiprocess but didn't find process with package: "
+ "starting server anyway" );
}
}
} else {
startServer = true ;
}
if (startServer) {
Server.create(AppInfo.applicationId, BootstrapApplication.this );
}
} catch (Throwable t) {
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Failed during multi process check" , t);
}
Server.create(AppInfo.applicationId, BootstrapApplication.this );
}
}
if (realApplication != null ) {
realApplication.onCreate();
}
}
这里分成三部分,资源部署->开启Server->realAppication正式启动。第三步不用多说,这里简单介绍一下前两步。第一步所做的事情就是将我们自己所写的实际上的应用加载进这个宿主程序中来,这就是我们所常常会听到的应用插件化中的一个概念“双开”,这里先不多说来看看第二步。从Server这个类的名字我们就可以猜到它与我们实际运行的程序实际上是一个C/S的关系(Google好像很喜欢搞C/S结构的东西,Android源码中就到处都是C/S架构),那它的作用是什么呢???
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
public static void create (@NonNull String packageName, @NonNull Application application) {
new Server(packageName, application);
}
private Server (@NonNull String packageName, @NonNull Application application) {
mApplication = application;
try {
mServerSocket = new LocalServerSocket(packageName);
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Starting server socket listening for package " + packageName
+ " on " + mServerSocket.getLocalSocketAddress());
}
} catch (IOException e) {
Log.e(LOG_TAG, "IO Error creating local socket at " + packageName, e);
return ;
}
startServer();
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Started server for package " + packageName);
}
}
private void startServer () {
try {
Thread socketServerThread = new Thread(new SocketServerThread());
socketServerThread.start();
} catch (Throwable e) {
if (Log.isLoggable(LOG_TAG, Log.ERROR)) {
Log.e(LOG_TAG, "Fatal error starting Instant Run server" , e);
}
}
}
我们看到这里开起了一个线程并创建了一个Socket,该Socket所做的事情是对我们的IDE进行监听,当我们需要将我们所写的代码需要被部署的时候Server端便会做点什么。那么我们来看看这个线程都做了些什么。
Server.SocketServerThread
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
private class SocketServerThread extends Thread {
@Override
public void run () {
......
while (true ) {
try {
LocalServerSocket serverSocket = mServerSocket;
if (serverSocket == null ) {
break ;
}
LocalSocket socket = serverSocket.accept();
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Received connection from IDE: spawning connection thread" );
}
SocketServerReplyThread socketServerReplyThread = new SocketServerReplyThread(
socket);
socketServerReplyThread.run();
if (sWrongTokenCount > 50 ) {
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Stopping server: too many wrong token connections" );
}
mServerSocket.close();
break ;
}
} catch (Throwable e) {
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Fatal error accepting connection on local socket" , e);
}
}
}
}
}
这里我们发现,当Socket所在的线程监听到我们的程序有改变的内容需要进行部署的时候,便会再次开启一个线程,莫非这个线程所作的事情就是我们所想要知道的更新部署操作???
Server.SocketServerReplyThread
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
private class SocketServerReplyThread extends Thread {
private final LocalSocket mSocket;
SocketServerReplyThread(LocalSocket socket) {
mSocket = socket;
}
@Override
public void run () {
try {
DataInputStream input = new DataInputStream(mSocket.getInputStream());
DataOutputStream output = new DataOutputStream(mSocket.getOutputStream());
try {
handle(input, output);
} finally {
try {
input.close();
} catch (IOException ignore) {
}
try {
output.close();
} catch (IOException ignore) {
}
}
} catch (IOException e) {
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Fatal error receiving messages" , e);
}
}
}
......
}
这里我们查看到了一个handle()
方法,该方法似乎将会对我们的Socket所监听到的内容进行处理。
Server.SocketServerReplyThread
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
private void handle (DataInputStream input, DataOutputStream output) throws IOException {
......
while (true ) {
int message = input.readInt();
switch (message) {
......
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();
output.writeBoolean(true );
restart(updateMode, hasResources, showToast);
continue ;
}
......
}
}
}
该方法中,Server从Socket中获取到了输入信息message,对于其余的输入信息我们不多做研究,我们主要关注的就是当输入信息为MESSAGE_PATCHES
时所要做的事情,这里我们发现了handlePatches()
函数,从名字里我们就可以看出来我们终于找到了部署操作的入口。
小结:在开始分析部署功能之前,我们对Instant Run运行过程做一个简单的小结。Instant Run是一个宿主程序,我们自己所写的程序便是运行在这个宿主程序之中的,而这个宿主程序中存在着一个C/S架构,Server端负责时刻监视我们的所需运行的程序,如果程序代码发生了改变,便会立刻将改变的内容进行部署。
部署功能解析 经过我们对Instant Run运行过程的分析,我们总算是找到了部署功能的入口,我们来看看它都是怎么做的。
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
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.endsWith(CLASSES_DEX_SUFFIX)) {
handleColdSwapPatch(change);
boolean canHotSwap = false ;
for (ApplicationPatch c : changes) {
if (c.getPath().equals(RELOAD_DEX_FILE_NAME)) {
canHotSwap = true ;
break ;
}
}
if (!canHotSwap) {
updateMode = UPDATE_MODE_COLD_SWAP;
}
} else 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;
}
在具体分析之前,先了解一下冷、热、温部署都代表了什么:
热部署:方法内的简单修改,无需重启app和Activity
。
冷部署:继承关系的改变或方法的签名变化等情况,应用需要重启。
温部署:资源的修改等情况,app无需重启,但是Activity
需要重启。
接下来我们对着三种部署方式进行分析:
热部署过程
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
private int handleHotSwapPatch (int updateMode, @NonNull ApplicationPatch patch) {
try {
String dexFile = FileManager.writeTempDexFile(patch.getBytes());
if (dexFile == null ) {
Log.e(LOG_TAG, "No file to write the code to" );
return updateMode;
} else if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Reading live code from " + dexFile);
}
String nativeLibraryPath = FileManager.getNativeLibraryFolder().getPath();
DexClassLoader dexClassLoader = new DexClassLoader(dexFile,
mApplication.getCacheDir().getPath(), nativeLibraryPath,
getClass().getClassLoader());
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) {
e.printStackTrace();
updateMode = UPDATE_MODE_COLD_SWAP;
}
} catch (Throwable e) {
updateMode = UPDATE_MODE_COLD_SWAP;
}
return updateMode;
}
这里是不是看得一头雾水?这里涉及的主要就是反射调用,这里我们基本可以分析出来内容的就是去获取一个AppPatchesLoaderImpl
类的实例,这个类是PatchesLoader
类的子类,然后反射调用了该实例等getPatchesClases()
方法。 不过我们发现,在Instant Run框架中并不存在AppPatchesLoaderImpl
类,不过我们可以找到的是PatchesLoader
,这是一个接口。
PatchesLoader
1
2
3
4
public interface PatchesLoader {
boolean load () ;
}
这里我们发现的是一个load()
方法,但是并没有getPatchedClasses()
方法啊,因此我们可以知道的是getPatchedClasses()
方法存在于PatchesLoader
的实现类AppPatchesLoaderImpl
中。 我们想要找到AppPatchesLoaderImpl
类还需要进入到实际运行在我们手机中的程序里找。 这里我构建了一个简单的应用,它的功能就是简单的打出嗝Toast。我们在手机中的/data/data/com.zpauly.simple路径下会找到我们的应用。这里注意,由于Instant Run实际上在我们手机上部署的是一个宿主应用,因此在/data/app/{applicationId}路径下的并不是我们自己的应用,而是一个宿主程序。 在/data/data/com.zpauly.simple/files/instant-run/dex-temp路径下面,我们找到了一个dex文件,这就是我们每次部署的时候所会加载的dex补丁包。我们将这个补丁包反编译下来就能看到这样的东西 我们发现了我们所需要的AppPatchesLoaderImpl
类!!!那我们就来看看里面都是什么东西吧。
AppPatchesLoaderImpl
1
2
3
4
5
6
7
8
9
10
11
12
package com.android.tools.fd.runtime;
public class AppPatchesLoaderImpl
extends AbstractPatchesLoaderImpl
{
public static final long BUILD_ID = 1490698846571L ;
public String[] getPatchedClasses()
{
return new String[] { "com.zpauly.simple.MainActivity" };
}
}
果然我们所需要的getPatchedClasses()
方法在这里,这里所返回的便是我们的需要进行修改的Class的完整类名。 虽然找到了getPatchedClasses()
方法,但是并没有找到loader()
方法。我们看到这里的AppPatchesLoaderImpl
继承的是AbstractPatchedLoaderImpl
,并非直接实现PatchesLoader
,那么loader()
方法等实现估计就是在这里了,我们看看AbstractPatchedLoaderImpl
。
AbstractPatchedLoaderImpl
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
public abstract class AbstractPatchesLoaderImpl implements PatchesLoader {
public abstract String[] getPatchedClasses();
@Override
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 , true );
}
}
changeField.set(null , o);
}
} catch (Exception e) {
if (logging != null ) {
logging.log(Level.SEVERE, String.format("Exception while patching %s" , "foo.bar" ), e);
}
return false ;
}
return true ;
}
}
这么一大堆反射是不是看得云里雾里。没关系,我这就来讲一下为什么这么做。 其实在我们的程序进行gradle编译的过程中,Instant Run会向我们的程序注入大量的代码,然后我们的MainActivity
就变成了下面这副模样。
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
public class MainActivity extends AppCompatActivity {
public static IncrementalChange $change = null ;
public MainActivity () {
}
MainActivity(Object[] paramArrayOfObject,
InstantReloadException paramInstantReloadException) {
}
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 );
Toast.makeText(this , "show toast" , Toast.LENGTH_SHORT).show();
}
}
实际上,在程序编译期间,Instant Run会为每一个类中的每一个方法中的最开始添加上一段代码,这段代码会对$change
变量进行判断,如果它不为null,那么我们自己的代码就不将会去执行,而是执行access$diapatch()
方法。 所以我们就知道了,AbstractPatchedLoaderImpl
中的load()
方法的作用就是去对所需进行改变的类中的$change
的静态变量进行赋值,这样就能够起到一种拦截的作用,我们原本的代码就将不会执行而会执行$change
中的access$dispatch()
方法。 在上面的load()
方法中,赋给变量$change
的内容是一个类型为MainActivity$override
的实例。我们再回到我们反编译的内容中看看这个类都做了什么。
MainActivity$override
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
public class MainActivity $override
implements IncrementalChange
{
public static Object init$args(MainActivity[] paramArrayOfMainActivity, Object[] paramArrayOfObject)
{
return new Object[] { { paramArrayOfMainActivity, new Object[0 ] }, "android/support/v7/app/AppCompatActivity.()V" };
}
public static void init$body(MainActivity paramMainActivity, Object[] paramArrayOfObject) {}
public static void onCreate (MainActivity paramMainActivity, Bundle paramBundle)
{
MainActivity.access$super (paramMainActivity, "onCreate.(Landroid/os/Bundle;)V" , new Object[] { paramBundle });
paramMainActivity.setContentView(2130968603 );
Toast.makeText(paramMainActivity, "show toast" , 0 ).show();
}
public Object access$dispatch(String paramString, Object... paramVarArgs)
{
switch (paramString.hashCode())
{
default :
throw new InstantReloadException(String.format("String switch could not find '%s' with hashcode %s in %s" , new Object[] { paramString, Integer.valueOf(paramString.hashCode()), "com/zpauly/simple/MainActivity" }));
case -641568046 :
onCreate((MainActivity)paramVarArgs[0 ], (Bundle)paramVarArgs[1 ]);
paramString = null ;
}
for (;;)
{
return paramString;
paramString = init$args((MainActivity[])paramVarArgs[0 ], (Object[])paramVarArgs[1 ]);
continue ;
init$body((MainActivity)paramVarArgs[0 ], (Object[])paramVarArgs[1 ]);
paramString = null ;
}
}
}
我们可以看到MainActivity$override
是IncrementalChange
类型的实现类,在它的access$dispatch()
方法中,它通过传入的方法名来选择执行的内容。当我们的MainActivity
执行了该方法并传入了onCreate()
方法名作时,MainActivity$override
便会执行它自己的onCreate()
方法来替代MainActivity
中原本的方法。 小结:这么一来,关于热部署的内容就很明了了,它所做的事情就是通过向我们所写的代码中的每一个方法的最开始注入一段拦截代码,这段拦截代码会对一个$change
变量进行判断,当我们的程序发生了改变需要部署时,Instant Run便会针对我们所需要改变的类生成补丁类,然后通过反射将我们的补丁类的实例赋给这个$change
变量,然后这段拦截代码便会成功拦截下原先的代码转而执行补丁类中的代码。
冷部署 看完了热部署,我们来看看冷部署。首先进入handleColdSwapPatch()
方法。
Server
1
2
3
4
5
6
7
8
9
private static void handleColdSwapPatch (@NonNull ApplicationPatch patch) {
if (patch.path.startsWith(Paths.DEX_SLICE_PREFIX)) {
File file = FileManager.writeDexShard(patch.getBytes(), patch.path);
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Received dex shard " + file);
}
}
}
这里会将我们所写的最新的程序等dex文件写入临时文件,然后当SocketServerReplyThread线程发现当前使用的是冷部署处理的时候,便会重启,在重启后宿主程序又将重新加载tmp目录中的dex文件,这样我们新的程序所编译生成的dex包便会被执行。
温部署 接下来再看看温部署。
Server
1
2
3
4
5
6
7
8
9
private static int handleResourcePatch (int updateMode, @NonNull ApplicationPatch patch,
@NonNull String path) {
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Received resource changes (" + path + ")" );
}
FileManager.writeAaptResources(path, patch.getBytes());
updateMode = Math.max(updateMode, UPDATE_MODE_WARM_SWAP);
return updateMode;
}
温部署我们上面提到过,就是当资源发生改变的时候所进行的部署方式。因此,这里的处理方式和上面的冷部署十分相似,就是将资源文件写入临时目录当中去,然后等待Activity
的重启。至于温部署和冷部署是如何将文件进行合并的,那都是插件化相关的内容,下回再谈。
关于热部署再说一点 关于热部署,其实Instant Run所做的事情就是灵活地运用反射在程序运行期间去动态的改变程序的运行轨迹。在这里要多说一点的是除了access$dispatch()
以外,Instant Run还在编译期注入了一个access$super()
方法。我们知道,在java的反射机制中是没有提供任何方式来调用父类方法的因此Instant Run才会注入这么一个方法,来提供父类方法的调用。当然,也并非只有这种方式才行,在美团的Robust框架中,就使用了修改指令集的方式来做到这点,这种方式的好处就在于免去了增加方法数的弊端。
总结 这里我对自己从Instant Run中所学到的关于热更新的思想做了一个简单的记录,下面我还会对其中的插件化思想做一个学习和记录。