2BAB's Engineering Blog

Android插件化笔记-7-MultiClassloader

Demo: https://github.com/2BAB/Android-Plugin-Dev-Notes

为什么要有 Multi Classloader

如上篇所说,我们不管要起 Activity、Service,其实都是需要注入自定义的 Classloader。而 Service 没有一个很好的简单注入点,所以才有了 Hook 上层 Classloader 的方案。这种方案有两种,都是解决多 Dex 加载的情况(不管插件化与否其实只要方法数超 65535 都是需要做多 Dex 加载):

  1. 单一 Classloader:就是上篇说到的 MultiDex 使用的反射注入 DexPathList 的前部,这种是利用了 BaseDexClassloader 的 findClass 特性,由前往后查找 Dex 文件并加载 Class;
  2. 多 Classloader:利用双亲委派的 Classloader 机制,使得我们的 Classloader 可以优先于系统 Classloader 查找到 Class 并返回,通常会伴随着每个模块一个 Classloader,再由一个 HookClassloader 统一 Dispatch;

目前淘宝、微店等都是使用多 Classloader 形式来实现 Dex 文件的动态加载,隔离性强、鲁棒性好,但实现上有所不同:

  1. 淘宝的 Atlas 做的是替换应用直接使用的 PathClassloader;
  2. 微店、Instant-Run 使用的是替换 PathClassloader 的 parent;

本系列的尿性就是要简单,稳定,尽量不 Hook 任何系统服务,所以下面以替换 PathClassloader 的 parent 思路来讲:

替换 PathClassloader 的 parent

很明显我们应该在应用还没启动的时候就把这事干了,所以参考 Instant-Run,Hook 时机在 Application 的 attachBaseContext 里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MultiClassloaderApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
replacePathClassloaderParent(base);
}
private void replacePathClassloaderParent(Context context) {
ClassLoader pathClassloader = context.getClassLoader();
DispatchClassloader dispatchClassloader = new DispatchClassloader(pathClassloader, context);
final Class<?> clz = ClassLoader.class;
try {
final Field parentField = clz.getDeclaredField("parent");
parentField.setAccessible(true);
parentField.set(pathClassloader, dispatchClassloader);
} catch (Exception e) {
e.printStackTrace();
}
}
}

实现 Multi Classloader

DispatchClassloader:

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
public class DispatchClassloader extends ClassLoader {
private BundleClassloader dexClassLoader;
private Context context;
private ClassLoader origin;
public DispatchClassloader(ClassLoader origin, Context context) {
super(origin.getParent());
this.origin = origin;
this.context = context;
installDex();
}
private void installDex() {
// 这里目前只装载了一个测试 Dex,正常情况下需要装载某个目录下的所有 dex 文件(通常每个 Bundle 有一个 Dex)
File optimizedDexOutputPath = new File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/7-MultiClassloader.dex");
File dexOutputDir = context.getDir("dex", 0);
dexClassLoader = new BundleClassloader(
optimizedDexOutputPath.getAbsolutePath(),
dexOutputDir.getAbsolutePath(),
null,
origin);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 需要在这里遍历所有 Bundle 的 Classloader,或者用包名等来做查找分发
Class<?> clz = dexClassLoader.findClass(name);
return clz;
}
}

BundleClassloader:

1
2
3
4
5
6
7
8
9
10
11
12
public class BundleClassloader extends DexClassLoader {
public BundleClassloader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
super(dexPath, optimizedDirectory, librarySearchPath, parent);
}
// 仅仅是用来改写 protected 签名
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
return super.findClass(name);
}
}

几个注意点

  1. 根据参考资料里 Google 的一段注释,如果不用 BundleClassloader 做查找转发的话,还有些隐藏 Bug。反正我们的目的本来就是需要多个 Classloader 的,就顺水推舟了;
  2. 请复写 findClass 而不是 loadClass,减少不必要的改动;
  3. findClass 默认 protected 的,所以需要继承 DexClassloader 改写 findClass 的签名;

Demo 工程打包过程

  • 先切到 /host 工程中,./gradlew installDebug 安装宿主工程;
  • 再切到 /plugin 工程中,按 之前文章(Android插件化笔记-2-LoadPluginClass)打包 Dex 的办法打出插件 Dex 文件并重命名为「7-MultiClassloader.dex」;
  • adb push 该文件到手机的 /sdcard/Downloads/目录下
  • 启动宿主工程,toast 出 3.14 即为成功;

参考资料:

本系列为笔记文,文中有大量的源码解析都是引用的其他作者的成果,详见下方参考资料。

讨论请发邮件到 xx2bab@gmail.com
自由转载-非商用-非衍生-保持署名 | Creative Commons BY-NC-ND 3.0