设计插件化框架
问题
如何设计一个 Android 插件化框架,实现动态加载未安装的 APK?
答案
核心挑战
插件化要让未安装的 APK 像正常 App 一样运行,需要解决三个核心问题:
| 问题 | 原因 | 方案 |
|---|---|---|
| 类加载 | 插件的 dex 不在宿主 ClassLoader 中 | 为插件创建独立 DexClassLoader |
| 资源加载 | 插件的资源不在宿主 AssetManager 中 | 通过反射添加插件 APK 的资源路径 |
| 组件生命周期 | 未注册的 Activity 无法被 AMS 启动 | Hook Instrumentation 或用占坑 Activity 代理 |
架构设计
类加载
class PluginClassLoader(
pluginApkPath: String,
optimizedDir: String,
nativeLibDir: String,
parent: ClassLoader
) : DexClassLoader(pluginApkPath, optimizedDir, nativeLibDir, parent) {
// 可覆写 findClass 实现"双亲委派"或"先插件后宿主"策略
override fun loadClass(name: String, resolve: Boolean): Class<*> {
// 先查自身缓存
var clazz = findLoadedClass(name)
if (clazz != null) return clazz
// 系统类交给父 ClassLoader
if (name.startsWith("java.") || name.startsWith("android.")) {
return parent.loadClass(name)
}
// 优先从插件 dex 中查找
return try {
findClass(name)
} catch (e: ClassNotFoundException) {
parent.loadClass(name)
}
}
}
Activity 代理(占坑方案)
在宿主 AndroidManifest.xml 中预注册若干占坑 Activity,启动插件 Activity 时用占坑 Activity 欺骗 AMS:
// Hook Instrumentation,拦截 startActivity
class PluginInstrumentation(
private val base: Instrumentation
) : Instrumentation() {
// 1. 发起启动 → 将目标替换为占坑 Activity,骗过 AMS
fun execStartActivity(/*...*/intent: Intent/*...*/): ActivityResult? {
val targetClass = intent.component?.className
// 把真正要启动的插件 Activity 信息保存到 extra
intent.putExtra("plugin_target", targetClass)
// 替换为宿主中已注册的占坑 Activity
intent.setClassName(hostPackage, "com.host.StubActivity")
return base.execStartActivity(/*...*/)
}
// 2. AMS 校验通过后,在真正创建 Activity 时换回插件类
override fun newActivity(cl: ClassLoader, className: String, intent: Intent): Activity {
val pluginTarget = intent.getStringExtra("plugin_target")
if (pluginTarget != null) {
// 用插件的 ClassLoader 加载真正的 Activity 类
return pluginClassLoader.loadClass(pluginTarget)
.newInstance() as Activity
}
return super.newActivity(cl, className, intent)
}
}
局限性
- Android 高版本对反射系统 API(
@hide)加了限制 - Google Play 明确禁止动态代码加载
- 维护成本高,每个 Android 版本都可能需要适配
- 主要用于国内超级 App(如微信、支付宝的小程序容器)
常见面试问题
Q1: 插件化和组件化的区别?
答案:
- 组件化:编译时将 App 拆分为多个 module,最终打包为一个 APK。主要解决工程管理问题(编译隔离、独立开发)
- 插件化:运行时动态加载未安装的 APK。主要解决动态化问题(热更新、功能按需下载)
组件化是工程架构方案,插件化是运行时加载方案。
Q2: 为什么需要占坑 Activity?
答案:
AMS(ActivityManagerService)在启动 Activity 时会检查 AndroidManifest.xml 中是否注册了该组件。插件 APK 未安装,其组件不在系统的 PackageManagerService 记录中,直接启动会抛 ActivityNotFoundException。占坑 Activity 是宿主中预注册的"空壳",用它通过 AMS 的校验,创建时再替换为真正的插件 Activity。