插件化基础(一)——加载插件的类

系列文章目录:

插件化基础(一)——加载插件的类
插件化基础(二)——加载插件资源
插件化基础(三)——启动插件组件


一、插件化概述

1.1 了解插件化

插件化技术最初源于免安装运行 apk 的想法,免安装的 apk 我们称之为插件,支持插件的 app 我们称之为宿主。宿主可以在运行时加载和运行插件,这样便可以将 app 中一些不常用的功能模块做成插件,既减小了安装包的体积,又实现了 app 功能的动态扩展。

插件化解决的问题:

  1. app 的功能模块越来越多,体积越来越大
  2. 模块之间的耦合度高,协同开发沟通成本越来越大
  3. 方法数目可能超过 65535,app 占用的内存过大
  4. 应用之间的互相调用

插件化与组件化的区别:

  • 组件化开发就是将一个 app 分成多个模块,每个模块都是一个组件,开发的过程中我们可以让这些组件相互依赖或者单独调试部分组件等,但是最终发布的时候是将这些组件统一合并成一个 apk,这就是组件化开发
  • 插件化开发和组件化略有不同,插件化开发是将整个 app 拆分成多个模块,这些模块包括一个宿主和多个插件,每个模块都是一个 apk,最终打包的时候宿主 apk 和插件 apk 分开打包

各个插件化框架对比:

插件化成本高,几乎每个源码版本都要适配。

同时,我们需要清楚,反射是插件化的基础,大量使用反射会影响性能,主要是因为:

  1. 产生大量的临时对象
  2. 过程中会进行可见性检查
  3. 会生成没有优化过的字节码
  4. 类型转换(基本类型的装箱拆箱)

1.2 插件化的实现思路

想要实现插件化需要解决以下三个问题:

  1. 如何加载插件的类?
  2. 如何加载插件的资源?
  3. 如何启动插件的四大组件?

文章也是围绕这三个问题展开的,总体大纲如下:

本篇文章先来看如何加载插件中的类,其实就是各种方式折腾 ClassLoader。

本系列文章所涉及的源码和运行环境默认为 Android 8.0(API 26),部分章节会涉及到对 8.0 以上系统的兼容适配,这部分会特别指出,没特殊说明的就默认为 8.0。

二、Android 的类加载机制

2.1 类的生命周期

先来看类的生命周期:

在加载阶段,虚拟机主要完成三件事:

  1. 通过一个类的全类名来获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在 Java 堆中生成一个代表这个类的 Class 对象,作为方法区数据的访问入口

2.2 Android 类加载器体系

Android 的类加载器与 Java 的类加载器有不同之处,前者用来加载 dex 文件,后者用来加载 Class 字节码文件。那么 Android 中的类加载器体系是怎样的呢?看下图:

PathClassLoader 与 DexClassLoader

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,这个“父”表示的不是继承关系上的父类子类,而是加载顺序的优先关系,一般都会先让自己的父加载器先执行加载,这一点在下面介绍“双亲委派机制”时会详细说。

以上源码能清晰的看到两点:

  1. PathClassLoader 被用作系统的类加载器,保存在 SystemClassLoader 类中,可以通过 getSystemClassLoader() 获取
  2. 该 PathClassLoader 的父加载器 parent 被指定为 BootClassLoader

也就是说,在 Context 环境下通过 getClassLoader() 得到的是一个 PathClassLoader,且这个 PathClassLoader 的父加载器是 BootClassLoader。

总结一下 PathClassLoader 和 DexClassLoader 的差别:

  1. 通过构造方法创建两个类加载器对象时,DexClassLoader 需要比 PathClassLoader 多传一个 optimizedDirectory 参数:
    ① 8.0 之前,必须要给 DexClassLoader 传 optimizedDirectory 参数,否则抛出异常
    ② 8.0 系统,也是必须要传 optimizedDirectory,但是内部已经不再使用该参数了,无实际意义
    ③ 8.1 开始,optimizedDirectory 可以传 null,不会抛出异常
  2. 通过构造方法创建两个类加载器对象时,可以在参数中指定它们的父加载器,当然如果没有需要也可以传 null
  3. 系统使用的 PathClassLoader 被指定了一个父加载器 BootClassLoader

BootClassLoader

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 的父加载器,二者加载的类也是不同的:

  • PathClassLoader 加载应用中的类(如我们写的 app 中的类,还有第三方依赖库中的类等)
  • BootClassLoader 加载 SDK(包括 Framework)的类(如 android.app 包下的 Activity)

2.3 类加载器的使用

创建一个 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();
        }
    }

2.4 双亲委派机制

源码分析

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;
    }

过程是:

  1. 先检查 name 表示的类是否已经被加载过,如果已经加载了,直接获取并返回
  2. 如果没有被加载过,且 parent 不为 null,就先让 parent 去加载,如果 parent 没加载到这个类才自己去加载

回看 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 也能构造出下图的关系):

使用双亲委派机制的原因

原因有二:

  1. 避免重复加载,当父加载器已经加载了该类的时候,就没有必要让子加载器再加载一次
  2. 安全性考虑,防止核心 API 库被随意篡改(保证系统类都是由系统的类加载器加载的)

关于第二点,稍微解释一下。比如像 Activity、String 这样系统中的类,开发者是不能自己创建一个包名与类名相同的类去让系统加载以达到篡改系统代码的目的,因为系统类会先由系统的 ClassLoader 加载,并且加载过的类不会再次加载。

2.5 类的加载

最终执行类加载的还是 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 中的方式实现插件类的加载。

这样类加载的流程就说完了,最后再总结一下吧:

在这里插入图片描述

过程概述:

  1. DexClassLoader/PathClassLoader 调用 loadClass() 进行类加载
  2. 直接进入父类 ClassLoader 的 loadClass(),开启“三步走”流程(对应 ClassLoader 触发的 1、2、3 号方法)
  3. 执行 ClassLoader 的 1 号方法 findLoadedClass() 看看待加载类是否已经被加载过,最终会调用到 VMClassLoader 的 native 方法,如果被加载过就直接返回给调用者 DexClassLoader/PathClassLoader
  4. 如果第 3 步没找到,就执行 ClassLoader 的 2 号方法,调用父类 BootClassLoader 的 loadClass(),如果加载到就返回给调用者
  5. 如果第 4 步没找到,就调用自己的 findClass() 去加载,拿到 DexPathList 中的 dexElements 集合,遍历其中的 Element 元素,执行 loadClassBinaryName(),如果没找到就抛出 ClassNotFoundException

三、加载插件中的类

有两种方案可以加载插件中的类:单 ClassLoader 和多 ClassLoader:

  • 单 ClassLoader:将插件 ClassLoader 中的 pathList 字段合并到宿主的 ClassLoader 中,宿主与插件之间可以直接互相调用类和方法
  • 多 ClassLoader:为每个插件都生成一个 ClassLoader,加载插件中的类时要使用对应的 ClassLoader

3.1 单 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 数组体积增大。

3.2 多 ClassLoader

为每个插件都生成一个 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());
    }