19. 动态代码加载:DexClassLoader、PathClassLoader 与 PMS 的关系,插件化基础
动态代码加载,说白了就是让 App 在运行时去加载外部的 dex 或 apk 文件。这玩意儿是插件化的技术基石。我当年第一次接触插件化时,就被 ClassLoader 的委派机制绕得有点晕。今天咱们就把 DexClassLoader、PathClassLoader 和 PMS 之间的关系彻底捋清楚。
19.1 两个核心 ClassLoader:DexClassLoader vs PathClassLoader
Android 的类加载器,核心就两个:PathClassLoader 和 DexClassLoader。它们都继承自 BaseDexClassLoader。区别在哪?说白了就一句话:PathClassLoader 只能加载已安装应用的 dex,DexClassLoader 可以加载任意路径的 dex。
| 特性 | PathClassLoader | DexClassLoader |
|---|---|---|
| 加载路径 | /data/app/ 下的已安装 apk | 任意路径(SD卡、插件目录) |
| 优化目录 | 系统自动管理(/data/dalvik-cache) | 需要开发者指定(如 /data/data/包名/code_cache) |
| 典型用途 | 系统启动、主 App 类加载 | 插件化、热修复 |
你可能会问:为什么 Android 要搞两个?嗯,这是历史原因。早期 Android 的 PathClassLoader 不支持指定优化目录,后来才统一到 BaseDexClassLoader 里。但 API 层面还是保留了这两个类,方便区分使用场景。
核心区别一句话:PathClassLoader 是「官方认证」的加载器,DexClassLoader 是「野路子」加载器。插件化玩的就是这个「野路子」。
19.2 双亲委派机制与 PMS 的关联
ClassLoader 有个经典机制叫「双亲委派」。什么意思?就是每个类加载器在加载类时,先问自己的父加载器:「你加载过这个类吗?」如果父加载器说「没有」,自己才去加载。
Android 的类加载链是这样的:
BootClassLoader(加载 Android 框架类)
↑ 委派
PathClassLoader(加载 App 自己的类)
↑ 委派
DexClassLoader(加载插件类)
这里有个关键点:PMS(PackageManagerService)负责管理已安装应用的 dex 路径。当系统启动时,PMS 会扫描 /data/app/ 下的所有 apk,把它们的 dex 路径告诉 PathClassLoader。所以 PathClassLoader 能加载的类,本质上是 PMS 注册过的。
而 DexClassLoader 绕过了 PMS。它直接读取你指定的 dex 文件,自己管理 dex 的加载和缓存。这就是插件化的核心——不经过 PMS,直接加载外部代码。
我的经验:我曾经在插件化项目里踩过一个坑——插件 dex 里引用了宿主的一个类,但宿主类被混淆了。结果 DexClassLoader 在加载插件时,发现父加载器(PathClassLoader)里找不到那个类,直接抛 ClassNotFoundException。后来我学乖了,插件和宿主之间只通过接口通信,不直接依赖具体类。
19.3 插件化的核心流程
插件化说白了就是三步:
- 准备插件 dex:把插件 apk 或 dex 文件放到 App 的私有目录(比如 /data/data/包名/plugin/)
- 创建 DexClassLoader:指定插件路径和优化目录
- 反射调用插件类:通过 ClassLoader 加载插件里的 Activity、Service 或普通类
代码示例:
// 插件 dex 路径
String pluginPath = context.getFilesDir() + "/plugin/plugin.apk";
// 优化目录(必须是 App 私有目录)
String optDir = context.getCodeCacheDir().getAbsolutePath();
// 创建 DexClassLoader
// 参数:dexPath, optimizedDirectory, librarySearchPath, parent
DexClassLoader classLoader = new DexClassLoader(
pluginPath,
optDir,
null,
context.getClassLoader()
);
// 加载插件里的类
Class<?> pluginClass = classLoader.loadClass("com.plugin.MainActivity");
// 通过反射调用
Object pluginInstance = pluginClass.newInstance();
Method method = pluginClass.getMethod("doSomething");
method.invoke(pluginInstance);
注意:优化目录必须是一个可写目录,而且最好是 App 私有的。我曾经见过有人把优化目录设到 SD 卡上,结果 Android 4.4 以后 SD 卡没有执行权限,直接崩溃。嗯,这个坑我帮别人填过好几次。
19.4 插件化要解决的几个核心问题
光能加载类还不够,真正的插件化框架还要处理这些问题:
- 资源加载:插件里的 layout、drawable 怎么访问?需要创建独立的 AssetManager
- 四大组件生命周期:Activity 必须在 AndroidManifest 里注册,插件里的 Activity 怎么启动?主流方案是「占坑 Activity」+ 反射注入
- so 库加载:插件如果有 native 代码,需要处理 so 文件的解压和加载路径
- 类冲突:插件和宿主如果用了同一个库的不同版本,怎么隔离?
你想想看,这些问题每一个都不简单。但最底层的基石,始终是 DexClassLoader 和双亲委派机制。
19.5 知识体系图
下面这张图展示了动态代码加载的核心逻辑和插件化的技术栈:
19.6 避坑指南
最后分享几个我踩过的坑:
- 优化目录权限:DexClassLoader 的第二个参数必须是 App 可写的目录。我建议用
context.getCodeCacheDir(),这是 Android 官方推荐的 dex 优化缓存目录 - 多插件类加载器隔离:如果同时加载多个插件,每个插件最好有自己的 DexClassLoader 实例,否则类容易冲突
- 插件更新:替换插件 dex 文件后,必须重新创建 DexClassLoader。旧的 ClassLoader 不会感知文件变化
- Android 版本差异:Android 8.0 以后对 ClassLoader 的访问限制更严格,反射调用时要注意
我的习惯:我在做插件化框架时,会用一个 Map 来管理所有插件的 ClassLoader,key 是插件名,value 是 DexClassLoader 实例。这样方便统一管理和回收。
动态代码加载,说白了就是「绕过 PMS,自己管 dex」。理解了 ClassLoader 的委派机制和 PMS 的角色,插件化的底层原理你就掌握了七成。剩下的三成,是具体框架的工程实现和兼容性处理。
公众号:蓝海资料掘金营,微信deep3321