Android 插件化第一步:用 DexClassLoader 把另一个 APK 加载进来

上一章把目标定下来了。这一篇先干一件很简单的事情:宿主不安装 plugin.apk,但是可以创建插件里的 Java 对象。

1. 准备一个插件 APK

假设插件里有:

1
2
3
4
5
6
7
package com.demo.plugin;

public class PluginEntry {
public String hello() {
return "hello plugin";
}
}

把插件正常编译成 plugin.apk,放到宿主可以访问的位置。

宿主拿到 APK 路径以后,就可以创建 ClassLoader。

2. DexClassLoader

核心代码其实非常短:

1
2
3
4
5
6
7
8
9
File apkFile = new File(pluginPath);
File optDir = context.getDir("plugin_dex", Context.MODE_PRIVATE);

DexClassLoader loader = new DexClassLoader(
apkFile.getAbsolutePath(),
optDir.getAbsolutePath(),
apkFile.getParent(),
context.getClassLoader()
);

然后:

1
2
3
4
5
Class<?> clazz = loader.loadClass("com.demo.plugin.PluginEntry");
Object object = clazz.getDeclaredConstructor().newInstance();

Method method = clazz.getMethod("hello");
String result = (String) method.invoke(object);

到这里,其实已经完成了最核心的一半:

1
2
3
4
5
6
7
plugin.apk

DexClassLoader

PluginEntry.class

反射创建对象

3. 为什么还需要 Resources

代码能加载,不代表插件就能正常运行。

比如插件里面:

1
setContentView(R.layout.activity_plugin);

这个 layout 在插件 APK 中,不在宿主 APK 中。

所以我们还需要单独创建插件 Resources。

最简单的思路是根据插件 APK 创建 AssetManager,然后创建 Resources

不同 Android 版本在 AssetManager 的公开接口上有差异,教学版可以通过反射完成资源路径注入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
AssetManager assetManager = AssetManager.class
.getDeclaredConstructor()
.newInstance();

Method addAssetPath = AssetManager.class
.getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, pluginPath);

Resources hostRes = context.getResources();
Resources pluginRes = new Resources(
assetManager,
hostRes.getDisplayMetrics(),
hostRes.getConfiguration()
);

然后插件页面需要资源时,不再使用宿主 context.getResources(),而是使用插件 Resources。

4. PluginContext

这里很容易踩坑。

如果插件 Activity 里面直接使用宿主 Context,那么:

1
getResources()

拿到的还是宿主资源。

所以后面真正创建插件组件时,最好提供一个简单的 PluginContext,至少把下面几个方法接管掉:

1
2
3
4
getClassLoader()
getResources()
getAssets()
getPackageName()

核心关系变成:

1
2
3
PluginContext
├── ClassLoader -> PluginClassLoader
└── Resources -> PluginResources

这一步做完以后,插件已经可以在宿主进程里“活起来”了。

5. PluginManager

可以把前面的东西统一封装一下:

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
public class PluginManager {

private DexClassLoader classLoader;
private Resources resources;

public void load(Context context, String apkPath) {
File apk = new File(apkPath);
File dexDir = context.getDir("plugin_dex", Context.MODE_PRIVATE);

classLoader = new DexClassLoader(
apk.getAbsolutePath(),
dexDir.getAbsolutePath(),
apk.getParent(),
context.getClassLoader()
);

// 初始化 plugin Resources
}

public ClassLoader getClassLoader() {
return classLoader;
}

public Resources getResources() {
return resources;
}
}

实际项目里还应该保存插件 APK 的 PackageInfo、版本号、签名信息等。

6. 到这里还差什么

现在可以:

  • 加载插件类;
  • 创建普通插件对象;
  • 获取插件资源。

但是还不能直接启动插件 Activity。

因为 Android 系统根本不知道这个 Activity。

下一篇就开始处理最典型的 Activity 代理。

坚持原创技术分享,您的支持将鼓励我继续创作!