赞
踩
系列文章目录:
插件化基础(一)——加载插件的类
插件化基础(二)——加载插件资源
插件化基础(三)——启动插件组件
插件化技术最初源于免安装运行 apk 的想法,免安装的 apk 我们称之为插件,支持插件的 app 我们称之为宿主。宿主可以在运行时加载和运行插件,这样便可以将 app 中一些不常用的功能模块做成插件,既减小了安装包的体积,又实现了 app 功能的动态扩展。
插件化解决的问题:
插件化与组件化的区别:
各个插件化框架对比:
插件化成本高,几乎每个源码版本都要适配。
同时,我们需要清楚,反射是插件化的基础,大量使用反射会影响性能,主要是因为:
想要实现插件化需要解决以下三个问题:
文章也是围绕这三个问题展开的,总体大纲如下:
本篇文章先来看如何加载插件中的类,其实就是各种方式折腾 ClassLoader。
本系列文章所涉及的源码和运行环境默认为 Android 8.0(API 26),部分章节会涉及到对 8.0 以上系统的兼容适配,这部分会特别指出,没特殊说明的就默认为 8.0。
先来看类的生命周期:
在加载阶段,虚拟机主要完成三件事:
Android 的类加载器与 Java 的类加载器有不同之处,前者用来加载 dex 文件,后者用来加载 Class 字节码文件。那么 Android 中的类加载器体系是怎样的呢?看下图:
PathClassLoader 和 DexClassLoader 其实非常相似,特别是 8.0 以后,二者的差别微乎其微。它们都可以用来加载 dex(应用内外皆可)/ apk(无论是否安装) 文件,只不过在 8.1 之前,DexClassLoader 的构造方法需要指定由 dex 优化而来的 odex(dex2oat 的产物)目录的路径:
public class DexClassLoader extends BaseDexClassLoader {
// optimizedDirectory 就是优化 dex 所需要的目录
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}
}
而 PathClassLoader 直接用系统指定的路径,不用我们来指定:
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
DexClassLoader 和 PathClassLoader 的全部内容就只是以上的构造方法,它们加载类的逻辑都在父类的 BaseDexClassLoader 中,所以表面上看起来它们的差别就只有 optimizedDirectory 这个参数了。
实际上,在 8.0 的 BaseDexClassLoader 中,就已经不再使用 optimizedDirectory 这个参数了:
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(parent);
// 8.0 中第 4 个传的参数是 null ,在之前传的都是 optimizedDirectory
this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);
if (reporter != null) {
reporter.report(this.pathList.getDexPaths());
}
}
只不过在 8.0 上创建 DexClassLoader 时给 optimizedDirectory 传 null 会抛出异常而已,而到了 8.1,DexClassLoader 在构造方法中也直接把第二个参数传了 null:
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
所以综合来看,在 8.0 以上,PathClassLoader 和 DexClassLoader 的区别很小,但是有一点不可忽视,就是 PathClassLoader 被用作系统的类加载器,这里要看一下顶级父类 ClassLoader 的源码:
public abstract class ClassLoader { static private class SystemClassLoader { public static ClassLoader loader = ClassLoader.createSystemClassLoader(); } // 当前 ClassLoader 对象的父 ClassLoader private final ClassLoader parent; private static ClassLoader createSystemClassLoader() { String classPath = System.getProperty("java.class.path", "."); String librarySearchPath = System.getProperty("java.library.path", ""); // 第三个参数,给这个 PathClassLoader 的 parent 指定为 BootClassLoader return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance()); } private ClassLoader(Void unused, ClassLoader parent) { this.parent = parent; } protected ClassLoader(ClassLoader parent) { this(checkCreateClassLoader(), parent); } protected ClassLoader() { this(checkCreateClassLoader(), getSystemClassLoader()); } @CallerSensitive public static ClassLoader getSystemClassLoader() { return SystemClassLoader.loader; } }
每个 ClassLoader 对象内部都有一个父加载器 parent,这个“父”表示的不是继承关系上的父类子类,而是加载顺序的优先关系,一般都会先让自己的父加载器先执行加载,这一点在下面介绍“双亲委派机制”时会详细说。
以上源码能清晰的看到两点:
也就是说,在 Context 环境下通过 getClassLoader() 得到的是一个 PathClassLoader,且这个 PathClassLoader 的父加载器是 BootClassLoader。
总结一下 PathClassLoader 和 DexClassLoader 的差别:
BootClassLoader 是 ClassLoader 的直接子类,其源码就在 ClassLoader.java 中,它是一个单例类并且没有父加载器 parent:
class BootClassLoader extends ClassLoader { private static BootClassLoader instance; @FindBugsSuppressWarnings("DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED") public static synchronized BootClassLoader getInstance() { if (instance == null) { instance = new BootClassLoader(); } return instance; } public BootClassLoader() { // parent 传 null,即没有父加载器 super(null); } }
前面提到过,BootClassLoader 是系统的 PathClassLoader 的父加载器,二者加载的类也是不同的:
创建一个 DexClassLoader 对象并调用其 loadClass 方法。
DexClassLoader 的构造方法需要指定 dex/apk/jar 文件的路径,这里我们以 dex 文件为例。先用 build-tools 下的 dx.bat 工具(\Android\Sdk\build-tools\【build-tools版本】\dx.bat)将 Class 文件打包成 dex 文件:
# dx --dex --output=[输出文件名] [输入的Class文件路径,需与包名匹配]
F:\...\build\intermediates\javac\debug\classes>dx --dex --output=test.dex com/demo/mylibrary/Test.class
运行该命令前需要将工作目录切换到输入文件路径的上一层级,示例命令中就是 com 文件夹的上一层:classes,输出的 test.dex 也就是在 classes 目录下:
将 dex 文件存入 /sdcard/ 目录下,通过 ClassLoader 加载该 dex 中的类:
private void test() {
// 这里读取文件需要用到 STORAGE 权限,此外第一个参数除了 dex 文件以外也可以是 apk 文件。
// 最后一个参数是指定 dexClassLoader 的 parent,可以是 PathClassLoader,7.0 以上系统也可以是 null
DexClassLoader dexClassLoader = new DexClassLoader("/sdcard/test.dex",
getCacheDir().getAbsolutePath(), null, getClassLoader());
try {
// loadClass() 只能加载指定的 dex 文件中的类,并通过反射调用其中的方法
Class<?> clazz = dexClassLoader.loadClass("com.demo.mylibrary.Test");
Method method = clazz.getMethod("printLog");
method.invoke(clazz);
} catch (Exception e) {
e.printStackTrace();
}
}
ClassLoader 的 loadClass() 是如何加载一个类的?来看 ClassLoader 源码:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { // 先检查这个类是否已经加载过 Class<?> c = findLoadedClass(name); // 如果没加载过 if (c == null) { try { if (parent != null) { // 如果有 parent,就先委派 parent 调用其 loadClass() 去加载, // 这也就是“双亲委派”这个词的由来 c = parent.loadClass(name, false); } else { // findBootstrapClassOrNull() 其实就返回 null,因为该方法在 // ClassLoader 类中返回 null,且所有子类没有重写该方法 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { ... } // 如果 parent 也没找到,才自己去查找 if (c == null) { c = findClass(name); } } return c; }
过程是:
回看 Android 类加载器的体系图,BaseDexClassLoader 及其子类 DexClassLoader、PathClassLoader 都没有重写 ClassLoader 中的 loadClass(),也就是说它们都用的是这种加载机制。这种先由父加载器去加载一个类的机制被称为双亲委派机制。
注意 ClassLoader 另一个子类 BootClassLoader 因为没有父加载器,所以加载过程略有不同:
@Override
protected Class<?> loadClass(String className, boolean resolve)
throws ClassNotFoundException {
// 检查是否已经加载过 className 表示的类
Class<?> clazz = findLoadedClass(className);
// 如果没加载过,直接由自己去查找
if (clazz == null) {
clazz = findClass(className);
}
return clazz;
}
总体来说,类加载的流程图:
假如一个 DexClassLoader 在创建时指定系统的 PathClassLoader 为其 parent,又由于系统的 PathClassLoader 的 parent 为 BootClassLoader,所以整个双亲委派加载类的示意图如下(当然 DexClassLoader 也可以指定非系统的 PathClassLoader 为 parent,只要再额外指定这个 PathClassLoader 的 parent 为 BootClassLoader 也能构造出下图的关系):
原因有二:
关于第二点,稍微解释一下。比如像 Activity、String 这样系统中的类,开发者是不能自己创建一个包名与类名相同的类去让系统加载以达到篡改系统代码的目的,因为系统类会先由系统的 ClassLoader 加载,并且加载过的类不会再次加载。
最终执行类加载的还是 ClassLoader 中的 findClass 方法,我们需要了解体系中各个 findClass() 都是如何执行的。
基类 ClassLoader 的 findClass() 会直接抛异常,等待子类的重写:
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
BootClassLoader 会调用 Class 类的 native 方法去加载:
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
return Class.classForName(name, false, null);
}
而 BaseDexClassLoader 会执行 DexPathList 的同名方法,如果没找到会抛出 ClassNotFoundException:
private final DexPathList pathList; @Override protected Class<?> findClass(String name) throws ClassNotFoundException { List<Throwable> suppressedExceptions = new ArrayList<Throwable>(); Class c = pathList.findClass(name, suppressedExceptions); if (c == null) { ClassNotFoundException cnfe = new ClassNotFoundException( "Didn't find class \"" + name + "\" on path: " + pathList); for (Throwable t : suppressedExceptions) { cnfe.addSuppressed(t); } throw cnfe; } return c; }
DexPathList 会遍历 dexElements 集合:
private Element[] dexElements; public Class<?> findClass(String name, List<Throwable> suppressed) { for (Element element : dexElements) { // 去 Element 中查找类 Class<?> clazz = element.findClass(name, definingContext, suppressed); if (clazz != null) { return clazz; } } if (dexElementsSuppressedExceptions != null) { suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions)); } return null; }
其实 dexElements 中的元素 Element 表示的就是 dex 文件,或者是包含 dex 文件的 jar 包,我们可以通过将插件 apk 添加到 dexElements 中的方式实现插件类的加载。
这样类加载的流程就说完了,最后再总结一下吧:
过程概述:
有两种方案可以加载插件中的类:单 ClassLoader 和多 ClassLoader:
灵感来自于【2.5 类的加载】,Element[] dexElements 包含了 app 中所有的 class 文件,想办法将插件的 dexElements 与宿主的 dexElements 合并后再设置给宿主即可。
代码实现:
/** * 思路是将插件的 dexElements 与宿主的 dexElements 合并形成一个新的 * dexElements,再设置给宿主,这样宿主再通过 ClassLoader 加载时就可 * 以加载插件中的类了。 */ public static void loadPluginClass(Context context) { try { // 1.获取宿主的 dexElements Class<?> clazz = Class.forName("dalvik.system.BaseDexClassLoader"); Field pathListField = clazz.getDeclaredField("pathList"); pathListField.setAccessible(true); Class<?> dexPathListClass = Class.forName("dalvik.system.DexPathList"); Field dexElements = dexPathListClass.getDeclaredField("dexElements"); dexElements.setAccessible(true); // 拿到系统使用的那个 PathClassLoader 的 dexElements ClassLoader pathClassLoader = context.getClassLoader(); Object dexPathList = pathListField.get(pathClassLoader); Object[] hostElements = (Object[]) dexElements.get(dexPathList); // 2.获取插件的 dexElements DexClassLoader dexClassLoader = new DexClassLoader(PLUGIN_APK_PATH, context.getCacheDir().getAbsolutePath(), null, pathClassLoader); Object pluginPathList = pathListField.get(dexClassLoader); Object[] pluginElements = (Object[]) dexElements.get(pluginPathList); // 3.合并到新的 Element[] 中,先创建一个新数组 Object[] newElements = (Object[]) Array.newInstance(hostElements.getClass().getComponentType(), hostElements.length + pluginElements.length); // 宿主的 dexElements 放在插件的 dexElements 之前,而热修复时就刚好相反了 System.arraycopy(hostElements, 0, newElements, 0, hostElements.length); System.arraycopy(pluginElements, 0, newElements, hostElements.length, pluginElements.length); // 4.赋值 dexElements.set(dexPathList, newElements); } catch (Exception e) { e.printStackTrace(); } }
这种方式的优点是,宿主与插件之间可以直接互相调用类和方法,还可以将多个插件的公共模块抽取到一个 common 插件中供其它插件使用。
缺点也很明显,不同插件引用同一个库的不同版本时,可能会导致程序出错,需要进行特殊处理规避:
以上图为例,宿主和插件的 AppCompat 的版本不同,由于这个包中的类是系统的 PathClassLoader 进行加载的,那么一定是先加载了宿主的,而由于双亲委托机制的存在,已经加载过的类不会重复加载,导致插件中的 AppCompat 的类就不会加载,那么调用到 v1.0 与 v2.0 的差异代码时,就可能出现问题。
此外,当插件数量过多时,会造成宿主的 dexElements 数组体积增大。
为每个插件都生成一个 ClassLoader,加载插件中的类时要使用对应的 ClassLoader 对象,这样可以使不同插件的类隔离,当不同插件引用同一个类库的不同版本时,不会发生问题。参考代码如下:
public DexClassLoader getPluginClassLoader(Context context, String pluginPath) {
if (TextUtils.isEmpty(pluginPath)) {
throw new IllegalArgumentException("插件路径不能拿为空!");
}
File pluginFile = new File(pluginPath);
if (!pluginFile.exists()) {
Log.e(TAG, "插件文件不存在!");
return null;
}
File optDir = context.getDir("optDir", Context.MODE_PRIVATE);
return new DexClassLoader(pluginPath, optDir.getAbsolutePath(), null, context.getClassLoader());
}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。