Android插件化之ClassLoader

3,978 阅读9分钟

ClassLoader是由JVM平台提供的类加载器。它允许程序从网络、硬盘甚至是内存加载Class,这就为Android插件化提供了最基础的技术保障。Android平台对字节码文件作了优化,摒弃了传统JVM需要的.jar文件,而是采用体积更小的.dex文件。因此,Android自定义了一系列ClassLoader以满足对dex加载。本文分为两部分,第一部分介绍Android的ClassLoader机制;第二部分介绍Android ClassLoader机制在插件化中的应用。

Android的ClassLoader机制

类加载机制

为了表述方便,我们先来看一下《深入理解Java虚拟机》是对类加载机制怎么描述的:

Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校检、转换解析和初始化的,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。 与那些在编译时进行链连接工作的语言不同,在Java语言里面,类型的加载、连接和初始化都是在程序运行期间完成的,这种策略虽然会令类加载时稍微增加一些性能开销,但是会为Java应用程序提供高度的灵活性,Java里天生可以同代拓展的语言特性就是依赖运行期动态加载和动态链接这个特点实现的。例如,如果编写一个面相接口的应用程序,可以等到运行时在制定实际的实现类;用户可以通过Java与定义的和自定义的类加载器,让一个本地的应用程序可以在运行时从网络或其他地方加载一个二进制流作为代码的一部分,这种组装应用程序的方式目前已经广泛应用于Java程序之中。从最基础的Applet,JSP到复杂的OSGi技术,都使用了Java语言运行期类加载的特性。

Java虚拟机类加载分为5个过程:加载、验证、准备、解析和初始化。

在加载阶段,虚拟机需要完成以下3件事情:

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

而加载阶段的第一步通过一个类的全限定名来获取定义此类的二进制字节流被放到了JVM外部去实现,这就给了我们决定如何去获取所需要类的权利。实现这个动作的代码模块我们称为ClassLoader。

双亲委派模型

无论是JVM还是Android,它们在加载类的时候都遵循双亲委派模型。双亲委派模型是这样的,每一个类加载器都有一个父加载器,如果某个类加载器收到了加载类的请求,它不会自己处理,而是交给父加载处理。每一层的类加载器都会这样向上传递,因此所有的类加载请求都会到达顶层的根加载器。只有父加载器不能处理加载请求时,子加载器才会尝试处理。具体代码如下:

public abstract class ClassLoader {

	private ClassLoader parent;

	protected ClassLoader(ClassLoader parentLoader) {
        this.parent = parentLoader;
    }

    public Class<?> loadClass(String className) throws ClassNotFoundException {
        return loadClass(className, false);
    }

    protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
      	//查找类是否已经加载过
        Class<?> clazz = findLoadedClass(className);
	//类没有加载过
        if (clazz == null) {
            ClassNotFoundException suppressed = null;
            try {
              	//交给父加载器处理
                clazz = parent.loadClass(className, false);
            } catch (ClassNotFoundException e) {
                suppressed = e;
            }
	    //父加载器不能处理加载请求
            if (clazz == null) {
                try {
                    //当前类加载器处理加载请求
                    clazz = findClass(className);
                } catch (ClassNotFoundException e) {
                    e.addSuppressed(suppressed);
                    throw e;
                }
            }
        }
        return clazz;
    }

    protected Class<?> findClass(String className) throws ClassNotFoundException {
        throw new ClassNotFoundException(className);
    }
}

这段代码的解释如下:

  1. ClassLoader是一个抽象类。它的构造方法需要传入一个父加载器。
  2. ClassLoader提供一个public的loadClass方法,这个方法的作用就是根据类的全限定名加载类,它的内部调用了protected的loadClass。
  3. protected的loadClass是双亲委派模型的核心。先检查是否已经加载过,若没有加载过则调用父加载器加载,若父加载器加载失败,则调用自己的findClas()方法加载。

ClassLoader为我们提供了两个protected的方法loadeClass(string className, boolean resolve)和findClass(String className)。你也许会有疑惑,loadeClass(string className, boolean resolve)为什么要声明为protected的呢,这样子类岂不是可以重写这个方法从而绕过了双亲委派模型。其实,这是由于历史原因造成的。在Java初期,开发JDK的大脑袋们并没有提供findClass()方法,双亲委派模型需要开发者自己去维护。Java 1.2时,这些大脑袋们为了重构了ClassLoader,才有了findClass()方法,但是为了兼容之前的版本,loadClass()方法保留了protected声明。所以,为了安全起见,我们还是老老实实的重写findClass()方法吧。

Android中的ClassLoader

为了能够加载dex/apk文件,Android重新定义了一系列的ClassLoader。其中的PathClassLoader和DexClassLoader是本文分析的对象。

PathClassLoader

PathClassLoader是什么呢?要弄清楚这个问题需要对app的启动流程有个简单的认识。首先,apk在安装成功后会被存储在data/app/的目录下。然后,app启动时,系统会去data/app/目录下找到相应的apk加载到内存中。而这个加载动作就是通过PathClassLoader完成的。因此,PathClassLoader只能加载系统中已经安装过的apk,对应到插件化技术中就是加载宿主apk。

/**
 * Provides a simple {@link ClassLoader} implementation that operates on a list
 * of files and directories in the local file system, but does not attempt to
 * load classes from the network. Android uses this class for its system class
 * loader and for its application class loader(s).
 */
public class PathClassLoader extends 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);
    }
}

上面这段代码是PathClassLoader的全部代码了,我们可以看到它单纯的继承了BaseDexClassLoader。关于BaseDexClassLoader我们一会再说。

DexClassLoader

DexClassLoader是一种允许app运行期间加载外部jar/apk文件的加载器。因此我们用它来加载插件。

/**
 * A class loader that loads classes from {@code .jar} and {@code .apk} files
 * containing a {@code classes.dex} entry. This can be used to execute code not
 * installed as part of an application.
 *
 * <p>This class loader requires an application-private, writable directory to
 * cache optimized classes. Use {@code Context.getCodeCacheDir()} to create
 * such a directory: <pre>   {@code
 *   File dexOutputDir = context.getCodeCacheDir();
 * }</pre>
 *
 * <p><strong>Do not cache optimized classes on external storage.</strong>
 * External storage does not provide access controls necessary to protect your
 * application from code injection attacks.
 */
public class DexClassLoader extends BaseDexClassLoader {
  
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}

和PathClassLoader一样,DexClassLoader也只是继承了BaseDexClassLoader。看来,只要弄清楚了BaseDexClassLoader就能理解PathClassLoader和DexClassLoader的加载机制了。

BaseDexClassLoader

public class BaseDexClassLoader extends ClassLoader {
  
  	private final DexPathList pathList;

    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            throw new ClassNotFoundException();
        }
        return c;
    }
}

上面这段代码是BaseDexClassLoader加载类的关键代码,它还是非常简单的。

构造方法有四个参数,含义如下:

  • dexPath: 包含目标类或资源的apk/jar列表;当有多个路径则采用:分割。
  • optimizedDirectory: 优化后dex文件存在的目录, 可以为null。
  • libraryPath: native库所在路径列表;当有多个路径则采用:分割。
  • Parent:父类的类加载器。

在构造方法中通过this、dexpath、libraryPath、optimizedDirectory生成了一个DexPathList对象,并保存在pathList中。

重写的findClass()方法中,将加载类的具体逻辑交给了pathList对象。

我们接着了解DexPathList。

public class DexPathList{

    private final Element[] dexElements;
    private final ClassLoader definingContext;
  
    public DexPathList(ClassLoader definingContext, String dexPath,
            String libraryPath, File optimizedDirectory) {
        this.definingContext = definingContext;
        // save dexPath for BaseDexClassLoader
        this.dexElements = makePathElements(splitDexPath(dexPath), optimizedDirectory);
    }

    public Class findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {
            DexFile dex = element.dexFile;

            if (dex != null) {
                Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
                if (clazz != null) {
                    return clazz;
                }
            }
        }
        return null;
    }
}    

DexPathList类的代码很长,上面这段只列出了我们需要关心的,它的意思就是在new DexPathList时,会通过构造方法中的dexPath,optimizedDirectory生成一个Element[]数组,并保存在dexElements中。在真正加载类的findClass()方法中,遍历dexElements,通过Element的loadClassBinaryName加载Class。这里请记住dexElements,因为它在后文中很重要。

源码分析到这里就结束了,因为插件化不需要更深的知识了。如果你想了解Android ClassLoader整个加载流程,可以学习这篇文章Android类加载器ClassLoader

Android ClassLoader机制在插件化中的应用

我们在Android插件化开篇中提到过,插件化是将一个apk拆分成一个宿主和多个插件的技术。那必然有以下三个问题需要考虑:

  1. 插件如何加载宿主中的类。
  2. 宿主如何加载插件中的类。
  3. 一个插件如何加载其它插件中的类。

第一个问题比较简单,我们只需要在构造插件DexClassLoader时,把宿主PathClassLoader作为parent传递进去(双亲委托模型)。

第二个问题比较复杂,因为宿主PathClassLoade没办法直接拿到插件的信息。那有没有办法在运行期间动态向宿主PathClassLoader添加插件apk信息呢?答案是肯定的,它要靠上文提到的dexElements完成。我们在原理部分分析了宿主PathClassLoader加载类的动作实际上是遍历DexPathList的dexElements完成的,如果我们将插件DexClassLoader中的dexElements添加到宿主PathClassLoader中去,是不是宿主PathClassLoader也有了插件的信息了呢。

由于双亲模型,第二个问题解决了,第三个问题也自然就解决了。

具体的代码逻辑如下:

 protected ClassLoader createClassLoader(Context context, File apk, ClassLoader parent) throws Exception {

        File dexOutputDir = getDir(context, Constants.OPTIMIZE_DIR);
        String dexOutputPath = dexOutputDir.getAbsolutePath();
        File nativeLibDir = getDir(context, Constants.NATIVE_DIR);
        DexClassLoader loader = new DexClassLoader(apk.getAbsolutePath(), dexOutputPath, nativeLibDir.getAbsolutePath(), parent);
       
        DexUtil.insertDex(loader, parent);
        return loader;
    }
public class DexUtil {
    //将dexClassLoader的dexElements添加到baseClassLoader的dexElements中去。
    public static void insertDex(DexClassLoader dexClassLoader, ClassLoader baseClassLoader) throws Exception {
        Object baseDexElements = getDexElements(getPathList(baseClassLoader));
        Object newDexElements = getDexElements(getPathList(dexClassLoader));
        Object allDexElements = combineArray(baseDexElements, newDexElements);
        Object pathList = getPathList(baseClassLoader);
        Reflector.with(pathList).field("dexElements").set(allDexElements);
    }
    //通过反射获取dexElements
    private static Object getDexElements(Object pathList) throws Exception {
        return Reflector.with(pathList).field("dexElements").get();
    }
    //通过反射获取PathClassLoader/DexClassLoader中的pathList
    private static Object getPathList(ClassLoader baseDexClassLoader) throws Exception {
        return Reflector.with(baseDexClassLoader).field("pathList").get();
    }
    //合并数组
    private static Object combineArray(Object firstArray, Object secondArray) {
        Class<?> localClass = firstArray.getClass().getComponentType();
        int firstArrayLength = Array.getLength(firstArray);
        int secondArrayLength = Array.getLength(secondArray);
        Object result = Array.newInstance(localClass, firstArrayLength + secondArrayLength);
        System.arraycopy(firstArray, 0, result, 0, firstArrayLength);
        System.arraycopy(secondArray, 0, result, firstArrayLength, secondArrayLength);
        return result;
    }
}

Android插件化开篇中我说过,每一篇文章的最后都会是一个Demo,这些Demo串联起来就是一个插件化框架,所以我在Github上建了一个项目VirtualApkLike,Demo都会以不同的分支放到这里。本文的Demo在classloader分支上。