ZeusPlugin 是掌阅开源的一个插件补丁框架,最近在使用的过程中也一边在研究原理,在此记录一下。
项目GitHub地址:ZeusPlugin
Android插件化主要需要解决两个问题:代码加载和资源加载。
代码加载
系统类加载机制
双亲委托模型
Java的JVM虚拟机运行程序时,通过ClassLoader把需要的Class从jar文件加载到内存中。Android的Dalvik/ART虚拟机与标准Java的JVM虚拟机不一样,ClassLoader加载的是dex文件,但是两者工作机制类似,都实现了双亲委托模型(Parent-Delegation Model)。
ClassLoader的构造方法如下:
ClassLoader(ClassLoader parentLoader, boolean nullAllowed) {
if (parentLoader == null && !nullAllowed) {
throw new NullPointerException("parentLoader == null && !nullAllowed");
}
parent = parentLoader;
}
创建一个ClassLoader实例的时候,需要使用一个现有的ClassLoader实例作为新创建的实例的Parent(启动类加载器bootstrap class loader除外)。这样一来,一个Android应用里所有的ClassLoader实例都会被一棵树关联起来。在加载类时,ClassLoader会优先委托parent去查询。
JVM中ClassLoader通过defineClass方法加载jar里面的Class,而Android中这个方法被弃用了,取而代之的是loadClass方法:
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
Class<?> clazz = findLoadedClass(className); //在当前ClassLoader已加载过的类中查找
if (clazz == null) {
ClassNotFoundException suppressed = null;
try {
clazz = parent.loadClass(className, false); //在parent中查找
} catch (ClassNotFoundException e) {
suppressed = e;
}
if (clazz == null) {
try {
clazz = findClass(className); //在自己中查找
} catch (ClassNotFoundException e) {
e.addSuppressed(suppressed);
throw e;
}
}
}
return clazz;
}
可以看到,类的加载会首先查询缓存(已加载到内存的部分);然后查询Parent是否已加载过该类,如果已经加载过就直接返回Parent加载的类;最后才会在当前ClassLoader查询并加载。因此如果一个类被位于树根的ClassLoader加载过,这个类就不会被重新加载。
BaseDexClassLoader
Android的 Dalvik/ART 虚拟机是通过 dex 或者 包含 dex 的jar、apk 文件来加载,主要逻辑都在 ClassLoader 的子类 BaseDexClassLoader 中。实际开发过程中,一般使用它的子类DexClassLoader、PathClassLoader这些类加载器来加载类。
在 Android 中,App 安装到手机后,apk 里面的 class.dex 中的 class 均是通过 PathClassLoader 来加载的。在项目中验证一下:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ClassLoader classLoader = getClassLoader();
if (classLoader != null){
Log.i(TAG, classLoader.toString());
while (classLoader.getParent()!=null){
classLoader = classLoader.getParent();
Log.i(TAG, classLoader.toString());
}
}
}
输出结果:
dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.test.classloadertest-2/base.apk"],nativeLibraryDirectories=[/vendor/lib64, /system/lib64]]]
java.lang.BootClassLoader@3930d8b6
可以看见有2个Classloader实例,一个是BootClassLoader(系统启动的时候创建的),另一个是PathClassLoader(应用启动时创建的,用于加载apk里面的类)。
PathClassLoader、DexClassLoader
PathClassLoader 和 DexClassLoader 都是BaseDexClassLoader的子类,两者的构造函数:
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String libraryPath,
ClassLoader parent) {
super(dexPath, null, libraryPath, parent);
}
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
- dexPath : 包含 dex 的 jar 文件或 apk 文件的路径集
- libraryPath : 包含 C/C++ 库的路径集
- dexPath : 包含 class.dex 的 apk、jar 文件路径
- optimizedDirectory : 用来缓存优化的 dex 文件的路径,即从 apk 或 jar 文件中提取出来的 dex 文件
可以看到区别在于 optimizedDirectory 这个参数,PathClassLoader 中该参数为null,只能加载内部的dex,这些大都是存在系统中已经安装过的apk里面的。
DexClassLoader 中该参数不能为空,它可以加载外部的dex(包括jar/apk,以及sd卡中未安装的apk),这个dex会被复制到内部路径的optimizedDirectory。因此一般需要动态加载时使用 DexClassLoader 这个类。
ZeusPlugin实现策略
ZuesPlugin 的实现中为插件及补丁分别添加相应的ClassLoader,并仿照系统类加载的双亲委托模型,设置它们的父子关系。其ClassLoader设计如下:
通过反射修改系统的ClassLoader为ZeusClassLoader,其内包含多个ZeusPluginClassLoader。
每一个插件对应一个ZeusPluginClassLoader,当移除插件时则删除一个ZeusPluginClassLoader,加载一个插件则添加一个ZeusPluginClassLoader。
ZeusClassLoader的parent为原始APK的ClassLoader,而原始APK的ClassLoader的parent为ZeusHotfixClassLoader, ZeusHotfixClassLoader的parent为系统rom的ClassLoader。
- ZeusClassLoader.java
ZeusClassLoader是一个容器类,其内维护了一个ZeusPluginClassLoader的列表,可以动态添加或删除ZeusPluginClassLoader。加载类时会依次到各个ZeusPluginClassLoader查找。它的parent是原APK的ClassLoader。
ZeusClassLoader重写了loadClass方法如下:
@Override
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
Class<?> clazz = null;
try {
//先查找parent classLoader,这里实际就是系统帮我们创建的classLoader,目标对应为宿主apk
clazz = getParent().loadClass(className);
} catch (ClassNotFoundException ignored) {
}
if (clazz != null) {
return clazz;
}
//挨个的到插件里进行查找
if (mClassLoader != null) {
for (ZeusPluginClassLoader classLoader : mClassLoader) {
if (classLoader == null) continue;
try {
//这里只查找插件它自己的apk,不需要查parent,避免多次无用查询,提高性能
clazz = classLoader.loadClassByself(className);
if (clazz != null) {
return clazz;
}
} catch (ClassNotFoundException ignored) {
}
}
}
throw new ClassNotFoundException(className + " in loader " + this);
}
- ZeusPluginClassLoader.java
在ZeusClassLoader中已经查找过parent,因此插件类加载通过loadClassByself方法,直接在本身查找。
public Class<?> loadClassByself(String className) throws ClassNotFoundException {
Class<?> clazz = findLoadedClass(className);
if (clazz == null) {
clazz = findClass(className);
}
return clazz;
}
- ZeusHotfixClassLoader.java
ZeusHotfixClassLoader是宿主APK ClassLoader的parent,因此加载类时会先加载补丁中的类。它的loadClass方法多了查找宿主APK的部分,如果补丁中未找到所需的类就会使用原APK中的类。它的parent是系统rom的ClassLoader。
@Override
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
//先查找补丁自己已经加载过的有没有
Class<?> clazz = findLoadedClass(className);
if (clazz == null) {
try {
//查查parent中有没有,也就是android系统中的
clazz = getParent().loadClass(className);
} catch (ClassNotFoundException ignored) {
}
if (clazz == null) {
try {
//查查自己有没有,就是补丁中有没有
clazz = findClass(className);
} catch (ClassNotFoundException ignored) {
}
}
}
//查查child中有没有,child是设置进来的,实际就是宿主apk中有没有
if (clazz == null && mChild != null) {
try {
if (findLoadedClassMethod != null) {
clazz = (Class<?>) findLoadedClassMethod.invoke(mChild, className);
}
if (clazz != null) return clazz;
if (findClassMethod != null) {
clazz = (Class<?>) findClassMethod.invoke(mChild, className);
return clazz;
}
} catch (Exception ignored) {
}
}
if (clazz == null) {
throw new ClassNotFoundException(className + " in loader " + this);
}
return clazz;
}
- PluginManager.java
PluginManager是插件管理类,管理插件的初始化、安装、卸载、加载等。在程序初始化时,PluginManager的init方法会执行一些初始安装加载的操作,其中会调用loadInstalledPlugins方法,这个方法中会设置插件、补丁ClassLoader与原APK的ClassLoader的父子关系。
//非完整代码,删去了与ClassLoader无关的部分
private static void loadInstalledPlugins() {
synchronized (mLoadLock) {
//...some code...
//获取classloader设置classloader
ZeusClassLoader classLoader = null;
for (String pluginId : installedPluginMaps.keySet()) {
if (PluginUtil.isPlugin(pluginId)) {
if (classLoader == null) {
classLoader = new ZeusClassLoader(mBaseContext.getClassLoader()); //parent为原APK的ClassLoader
}
classLoader.addAPKPath(pluginId, PluginUtil.getAPKPath(pluginId),
PluginUtil.getLibFileInside(pluginId),
PluginUtil.getInstalledPathInfo(pluginId));
}
//热修复补丁,补丁一般只针对某个版本
if (PluginUtil.isHotFix(pluginId)) {
try {
loadHotfixPluginClassLoader(pluginId); //该方法中会重设ZuesHotfixClassLoader的父子关系
} catch (Exception e) {
e.printStackTrace();
}
}
}
//设置原始APK所使用的ClassLoader
if (classLoader != null) {
PluginUtil.setField(mPackageInfo, "mClassLoader", classLoader);
Thread.currentThread().setContextClassLoader(classLoader);
mNowClassLoader = classLoader;
}
//...some code...
}
}
最终系统、原程序、插件、补丁的ClassLoader之间的父子关系如下图所示:
*以上图片来自ZeusPlugin交流群中的PPT.
资源加载
系统资源加载
Android 中通过 ContextImpl 类的两个成员函数 getResources 和 getAssets 来访问资源。
getResources 返回的是 ContextImpl 的成员变量 mResources,这个Resources对象可以通过资源ID来访问那些被编译过的应用程序资源。
Resources 的成员变量 mAssets 是一个 AssetManager 对象,getAssets 方法通过ContextImpl 的成员变量 mResources 的成员函数 getAssets 来获得该 AssetManager 对象。
AssetManager 可以通过文件名来访问那些被编译过或者没有被编译过的应用程序资源文件,事实上,Resources类也是通过AssetManager类来访问那些被编译过的应用程序资源文件的,不过在访问之前,它会先根据资源ID查找得到对应的资源文件名。
Android应用程序除了要访问自己的资源之外,还需要访问系统的资源。应用程序进程通过一个单独的 Resources 对象和一个单独的 AssetManager 对象来管理系统资源,这两个对象分别是 Resources 类的成员函数 mSystem 和 AssetManager 类的成员函数 sSystem。
ContextImpl、Resourses和AssetManager的关系如图:
* 图片来自这里
AssetManager 中通过一个隐藏方法 addAssetPath 来添加资源:
/** Add an additional set of assets to the asset manager.
* This can be either a directory or ZIP file.
* Not for use by applications. Returns the cookie of the added asset,
* or 0 on failure.
*@{hide}
*/
public native final int addAssetPath(String path);
该方法是一个 JNI 方法,具体实现由 C++ 层的相应方法完成。C++ 层维护有一个记录asset_path 的Vector,调用 addAssetPath后会将参数 path 所描述的 apk 文件路径添加到 Vector 中。
系统资源文件 framework-res.apk 也是通过这个方法添加到 AssetManager 中,所以Resources中的 AssetManager 既可以访问系统也可以访问 apk 资源。
ZeusPlugin 实现策略
类似于系统的资源加载过程,想要访问插件、补丁的资源,只需要将它们的apk路径也通过 addAssetPath 添加到 AssetManager 中就行了。创建一个新的 AssetManager 对象,通过反射调用 addAssetPath 方法,然后以这个 AssetManager 为参数创建 Resources 对象,并反射修改系统使用的 Resources。
这一系列步骤在 PluginManager 中的 reloadInstalledPluginResources 方法中完成:
//非完整代码
private static void reloadInstalledPluginResources() {
try {
//创建AssetManager,反射调用addAssetPath
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, mBaseContext.getPackageResourcePath());
if (mLoadedPluginList != null && mLoadedPluginList.size() != 0) {
for (String id : mLoadedPluginList.keySet()) {
if (!PluginUtil.isHotfixWithoutResFile(id)) {
addAssetPath.invoke(assetManager, PluginUtil.getAPKPath(id));
}
}
}
//创建Resources
PluginResources newResources = new PluginResources(assetManager,
mBaseContext.getResources().getDisplayMetrics(),
mBaseContext.getResources().getConfiguration());
//替换Resources
PluginUtil.setField(mBaseContext, "mResources", newResources);
PluginUtil.setField(mPackageInfo, "mResources", newResources);
//...some code...
} catch (Throwable e) {
e.printStackTrace();
}
}
这个方法在程序初始化和加载新插件的时候都会调用,一旦插件发生变化就会使用一个新的 Resources,避免原来 Resources 的缓存造成影响。
参考
https://github.com/iReaderAndroid/ZeusPlugin/wiki
https://segmentfault.com/a/1190000004062880
http://www.jianshu.com/p/22230ed1b6e2
http://jaeger.itscoder.com/android/2016/08/27/android-classloader.html
http://blog.csdn.net/luoshengyang/article/details/8791064