Instant Run学习的思考-插件化

在前面一篇文章里,已经讲过了Google的Instant Run给我们带来的一个技术上的启发——热修复,在这篇里我就紧接着上一篇来对Instant Run的另一个技术启发——插件化,做一个记录和总结。
对于Instant Run的基本的加载流程,我在上一篇里已经做了记录和总结,这里就不多做介绍了。

设置ClassLoader

我们知道,插件化简单来说就是一个应用从另一个dex或者一个apk中读取内容然后将其加载进来。但是这里有一个问题,就是要想将原本不属于自己的字节码文件加载进虚拟机中,原本的classloader固然是做不到的,那么就肯定要为需要加载的文件单独设置一个classloader才行。我们来看看Instant Run是怎么做的。

BootstrapApplication

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private static void setupClassLoaders(Context context, String codeCacheDir, long apkModified) {
List<String> dexList = FileManager.getDexList(context, apkModified);
if (!dexList.isEmpty()) {
//获取原本的classloader
ClassLoader classLoader = BootstrapApplication.class.getClassLoader();
String nativeLibraryPath;
//获取nativeLibraryPath
try {
nativeLibraryPath = (String) classLoader.getClass().getMethod("getLdLibraryPath")
.invoke(classLoader);
} catch (Throwable t) {
nativeLibraryPath = FileManager.getNativeLibraryFolder().getPath();
}
//单独设置一个classloader并将其加入进来
IncrementalClassLoader.inject(
classLoader,
nativeLibraryPath,
codeCacheDir,
dexList);
}
}

这个函数并没有什么问题,重点是IncrementalClassLoader.inject()方法。

IncrementalClassLoader

1
2
3
4
5
6
7
8
9
10
11
public static ClassLoader inject(
ClassLoader classLoader, String nativeLibraryPath, String codeCacheDir,
List<String> dexes) {
//创建IncrementalClassLoader的一个实例
IncrementalClassLoader incrementalClassLoader =
new IncrementalClassLoader(classLoader, nativeLibraryPath, codeCacheDir, dexes);
//将incrementalClassLoader设置为原本的classloader的双亲
setParent(classLoader, incrementalClassLoader);
return incrementalClassLoader;
}

IncrementalClassLoader是一个继承了ClassLoader类的自定义classloader,我们先来看看它的创建过程。

IncrementalClassLoader

1
2
3
4
5
6
7
8
public IncrementalClassLoader(
ClassLoader original, String nativeLibraryPath, String codeCacheDir, List<String> dexes) {
super(original.getParent());
//就是创建一个delegateClassLoader
delegateClassLoader = createDelegateClassLoader(nativeLibraryPath, codeCacheDir, dexes,
original);
}

在这里创建的delegateClassLoader究竟起了什么作用呢?我们知道ClassLoader是通过findClass()方法来寻找需要加载的类的,那来看看这个方法。

IncrementalClassLoader

1
2
3
4
5
6
7
8
9
public Class<?> findClass(String className) throws ClassNotFoundException {
try {
//实际上进行加载的classloader是degelateClassLoader
Class<?> aClass = delegateClassLoader.findClass(className);
return aClass;
} catch (ClassNotFoundException e) {
throw e;
}
}

原来IncrementalClassLoader其实只是一个代理,真正负责寻找类的是DelegateClassLoader

DelegateClassLoader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//DelegateClasser是IncrementalClassLoader的静态内部类,它继承自BaseDexClassLoader
private static class DelegateClassLoader extends BaseDexClassLoader {
private DelegateClassLoader(
String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
super(dexPath, optimizedDirectory, libraryPath, parent);
}
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
try {
Class<?> aClass = super.findClass(name);
return aClass;
} catch (ClassNotFoundException e) {
throw e;
}
}
}

果真,作为一个继承自BaseDexClassLoader的类,DelegateClassLoader才是真正的加载者。
对Android中类的加载过程有所了解的同学都知道,在Android中,Google自定义了一个BaseDexClassLoader类,它持有一个DexPathList的成员变量,这个成员变量中的便是需要加载的dex文件列表。Android中通常所使用的类加载器PathClassLoader便是继承自BaseDexClassLoader并从中读取dex文件列表并加载自己所需要的类。
现在我们知道了IncrementalClassLoader的作用便是使用DelegateClassLoader来进行dex文件的加载。那我们再来看看setParent()方法是做了什么。

IncrementalClassLoader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static void setParent(ClassLoader classLoader, ClassLoader newParent) {
try {
//将原本的classloader中的parent成员变量重新赋值为IncrementalClassLoader的实例
Field parent = ClassLoader.class.getDeclaredField("parent");
parent.setAccessible(true);
parent.set(classLoader, newParent);
} catch (IllegalArgumentException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}

这里的逻辑很简单,不过这里涉及到了类加载器的双亲委托机制,简单的讲就是一个classloader如果找不到它所需的类时,便会委托它的双亲classloader去寻找。因此在setParent()方法执行了之后,便会出现这么一个效果,原本的classloader由于只能加载宿主程序而找不到我们所需加载的dex文件,因此便会委托它的双亲——IncrementalClassLoader去寻找,最后便会由获取到我们的dex文件列表的DelegateClassLoader找到然后加载进虚拟机中。


Patch过程

patch过程要从这么两句代码讲起。

BootstrapClassLoader

1
2
3
4
5
6
MonkeyPatcher.monkeyPatchApplication(
BootstrapApplication.this, BootstrapApplication.this,
realApplication, externalResourcePath);
MonkeyPatcher.monkeyPatchExistingResources(BootstrapApplication.this,
externalResourcePath, null);

我们依次进入来分析一下。

MonkeyPatcher

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
public static void monkeyPatchApplication(@Nullable Context context,
@Nullable Application bootstrap,
@Nullable Application realApplication,
@Nullable String externalResourceFile) {
try {
//Step1.将ActivityThread中所有的Application替换为realApplication
Class<?> activityThread = Class.forName("android.app.ActivityThread");
Object currentActivityThread = getActivityThread(context, activityThread);
Field mInitialApplication = activityThread.getDeclaredField("mInitialApplication");
mInitialApplication.setAccessible(true);
Application initialApplication = (Application) mInitialApplication.get(currentActivityThread);
if (realApplication != null && initialApplication == bootstrap) {
mInitialApplication.set(currentActivityThread, realApplication);
}
if (realApplication != null) {
Field mAllApplications = activityThread.getDeclaredField("mAllApplications");
mAllApplications.setAccessible(true);
List<Application> allApplications = (List<Application>) mAllApplications
.get(currentActivityThread);
for (int i = 0; i < allApplications.size(); i++) {
if (allApplications.get(i) == bootstrap) {
allApplications.set(i, realApplication);
}
}
}
//Step2.将所有的LoadApk或是PackageInfo中的Application替换为realApplication以及另外一些替换工作
Class<?> loadedApkClass;
try {
loadedApkClass = Class.forName("android.app.LoadedApk");
} catch (ClassNotFoundException e) {
loadedApkClass = Class.forName("android.app.ActivityThread$PackageInfo");
}
Field mApplication = loadedApkClass.getDeclaredField("mApplication");
mApplication.setAccessible(true);
Field mResDir = loadedApkClass.getDeclaredField("mResDir");
mResDir.setAccessible(true);
Field mLoadedApk = null;
try {
mLoadedApk = Application.class.getDeclaredField("mLoadedApk");
} catch (NoSuchFieldException e) {
}
for (String fieldName : new String[]{"mPackages", "mResourcePackages"}) {
Field field = activityThread.getDeclaredField(fieldName);
field.setAccessible(true);
Object value = field.get(currentActivityThread);
for (Map.Entry<String, WeakReference<?>> entry :
((Map<String, WeakReference<?>>) value).entrySet()) {
Object loadedApk = entry.getValue().get();
if (loadedApk == null) {
continue;
}
if (mApplication.get(loadedApk) == bootstrap) {
//将Application替换为realApplication
if (realApplication != null) {
mApplication.set(loadedApk, realApplication);
}
//将mResDir替换为我们自己的apk文件中的res文件路径
if (externalResourceFile != null) {
mResDir.set(loadedApk, externalResourceFile);
}
//将Application中的LoadApk替换为我们自己的LoadApk
if (realApplication != null && mLoadedApk != null) {
mLoadedApk.set(realApplication, loadedApk);
}
}
}
}
} catch (Throwable e) {
throw new IllegalStateException(e);
}
}

这段代码虽然很长,但是所做的事情实际上很简单,就是一个词——替换,将运行期间所有的Application替换为realApplication,将所有原本的asset目录路径替换为我们自己的asset目录路径。当然,如果想要弄清楚,光是看这段代码肯定还是会有所难以理解的,一定要结合着Android中的源码一起看才行。
再来看看MonkeyPatcher.monkeyPatchExistingResources()方法。

MonkeyPatcher

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
public static void monkeyPatchExistingResources(@Nullable Context context,
@Nullable String externalResourceFile,
@Nullable Collection<Activity> activities) {
if (externalResourceFile == null) {
return;
}
try {
//的事情也是相当的简单,就是将运行期间所有的原asset路径和Resources对象都替换为我们自己的项目的asset路
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");
}
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);
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) {
}
pruneResourceCaches(resources);
}
}
Collection<WeakReference<Resources>> references;
if (SDK_INT >= KITKAT) {
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);
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) {
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);
}
}

这段方法虽然很长,但是实际上做的事情也是相当的简单,就是将运行期间所有的原asset路径和Resources对象都替换为我们自己的项目的asset路径和Resources对象。

总结

在之前我也看过一些有关插件化的文章和框架,以前的博客中也对看到资料也有所提及,但Instant Run所提供的思路和其他的都有所不同。
首先,想要加载插件中的内容,首先一定要做的事情就是要生成一个对应的classloader去专门对插件进行寻找和加载工作。
在对于Java层的插件的加载上,最主要需要注意的就是将运行期间所有原本的Application替换为我们自己的realApplication。这么做的主要的原因就是这样可以将原本的环境由宿主程序切换为我们自己的应用,这样有了正常的环境便无需再通过其他手段去加载我们自己应用的四大组件了。
插件化除运行环境外的另一个重点就是对于资源文件的加载。这里提供的方案就是将所有的asset和res等资源路径都通过反射替换为我们自己的应用中的路径,简单高效。
当然,Instant Run毕竟是对于Java层做的插件化工作,对于令人头疼的native层并没有做任何其他的工作,不过这对于大部分应用来说已经足够了。