Android插件化之ClassLoader

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

Android的ClassLoader机制

类加载机制

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

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

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

在加载阶段,虚拟机须要完成如下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()方法吧。app

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分支上。

相关文章
相关标签/搜索