ClassLoader解析——Android篇

我们在上一篇文章中,学习到Java中的ClassLoader的加载顺序以及双亲委托机制。但是在Android中的ClassLoader又有点不一样,Android重写了整个ClassLoader。我们来了解一下Android的ClassLoader机制

概述

Java的虚拟机是JVM,Android虽说是基于JAVA,但是为了更适应手机的特性,Android使用了自己特有的Dalvik/ART虚拟机

虽说是另一个虚拟机,但是ClassLoader的机制依旧存在,而且相似,Android的ClassLoader一样有特定的加载顺序和双亲委托机制

Dalvik/ART 虚拟机同样依靠ClassLoader来加载对应的类,但是不同于Java,Android在打包apk时并不是直接把class文件打包,而是对class文件优化之后生成dex文件,Android将所有的class文件打包成一个或多个(multiDex)文件。

然后在安装App时,Android虚拟机会进一步对apk中的dex文件进行优化

  • Dalivk虚拟机会使用DexOpt提取apk中的dex文件进一步优化,生成一个ODEX文件存储在缓存路径(/data/dalvik-cache/)下,而后打开APP可以直接加载ODEX文件而不用解析apk

  • 而ART虚拟机则会将apk中的dex文件优化为机器指令,保存为OAT文件于缓存路径下(/data/dalvik-cache/),不同于ODEX文件,CPU不需要再去解析OAT文件,因为里面已经是机器指令,这样的机制大大提高了运行效率,不过相对的占用空间就变大了

Android特有的ClassLoader

ClassLoader

Android重写了ClassLoader,我们先来看一下ClassLoader的重点代码:

public abstract class ClassLoader {
    ClassLoader(ClassLoader parentLoader, boolean nullAllowed) {
        // ……省略
        parent = parentLoader;       
    }
    
    protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
        // 省略部分代码
        // 查找已经加载的类
        Class<?> clazz = findLoadedClass(className);
        if (clazz == null) {
            // 委托parent加载
            clazz = parent.loadClass(className, false);
            if (clazz == null) {
                // 自己加载,空方法,交由子类实现
                clazz = findClass(className);
            }
        }
        return clazz;
    }    
}

可以看到ClassLoader同样是拥有parent和双亲委托原则,逻辑基本和Java的一样。
不过可以看到Android中废弃了Java中将jar文件转换为Class的方法defineClass,而一般子类会将该过程交由JNI实现。

BootClassLoader

不过,我们可以看到ClassLoader文件下还有另一个类:

/**
 *   位于其他ClassLoader的顶层,内部基于JNI实现
 */
class BootClassLoader extends ClassLoader {
    private static BootClassLoader instance;
    public static synchronized BootClassLoader getInstance() {
        if (instance == null) {
            instance = new BootClassLoader();
        }
        return instance;
    }

    public BootClassLoader() {
        // parent置为null
        super(null, true);
    }
    
    @Override
    protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
        Class<?> clazz = findLoadedClass(className);
        // 因为parent为null,所以跳过了调用parent.loadClass这一步
        if (clazz == null) {
            clazz = findClass(className);
        }
        return clazz;
    }    

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 通过JNI实现
        return Class.classForName(name, false, null);
    }
}

看这个名字,我们立刻就联想到JVM中的BootStrapClassLoader,没错,这个ClassLoader正是位于其他ClassLoader的顶层,也就是虚拟机第一个加载的ClassLoader,同样这个类的实际实现是基于JNI的。

该类负责加载Android的核心类库,如StringActivity等。

BaseDexClassLoader

看完这个类,我们再来看看ClassLoader的子类:BaseDexClassLoader:

/*
 *  解析Dex文件的ClassLoader的基类
 */
public class BaseDexClassLoader extends ClassLoader {
    public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
        super(parent);
        this.originalPath = dexPath;
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }
    
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        //……省略,主要基于JNI
    }    
}

这里提取了只提取了其中重要部分的代码,可以看到他的构造函数传入了dexPath等参数,而findClass方法主要基于JNI,正如他的注释所说,这是findClass通过JNI解析路径下的dex文件。

我们来重点看一下他的参数:

  • dexPath
    待解析文件所在的全路径,classloader将在该路径中指定的dex文件寻找指定目标类
  • optimzedDirectory
    优化路径,指的是虚拟机对于apk中的dex文件进行优化后生成文件存放的路径,如dalvik虚拟机生成的ODEX文件路径和ART虚拟机生成的OAT文件路径。
    这个路径必须是当前app的内部存储路径,Google认为如果放在公有的路径下,存在被恶意注入的危险
  • libraryPath
    指定native层代码存放路径
  • parent
    当前ClassLoaderparent,和java中classloaderparent含义一样

我们前面说了,Dalvik/ART虚拟机在第一次安装apk时,会对dex文件进行优化,存放到缓存路径,后续是直接读取缓存路径下的文件,而不再读取原文件,这里的optimzedDirectory正是指的优化后的缓存路径。

所以BaseDexClassLoaderloadClass会执行的一个流程大致如下(因为底层的JNI实现所以这里不看源码了,有兴趣可自行了解,这里只说结论):

  1. 判断optimzedDirectory路径下是否有对应的优化过的文件(ODEX/OAT)
  2. 如果步骤1判断否,那么解析dexPath路径指定的dex文件,进行优化并存储到optimzedDirectory路径下,否则直接进入步骤3
  3. 读取optimzedDirectory路径下对应的文件
  4. 解析为Class

PathDexClassLoader

接下我们看一下BaseDexClassLoader的子类,他有两个子类,DexClassLoaderPathClassLoader,我们分别看一下:

/**
 * 提供一个简单的ClassLoader去加载路径下指定的dex/jar/apk文件
 * Android系统通过该ClassLoader去加载系统应用类和App应用
 * Android建议我们不应该使用该类去加载我们自定义的类而是使用
 * DexClassLoader
 */
public class PathClassLoader extends BaseDexClassLoader {
    /**
     * @param dexPath 指定的dex/jar/apk文件的路径,可以包含多个路径,
     * 用{File.pathSeparator}分割。
     * @param parent 
     */
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }
    
    // 与上一个构造函数类似,只是多了一个libraryPath表示Native库路径
    public PathClassLoader(String dexPath, String libraryPath,
            ClassLoader parent) {
        super(dexPath, null, libraryPath, parent);
    }
}

可以看到,PathClassLoader里除了构造函数没有其他方法,所以根本的逻辑还是基于BaseDexClassLoader来完成。
特殊的一点是,我们发现PathClassLoader指定了optimzedDirectory为null??
这是为什么?

这个问题需要我们进入JNI才能解答,这里贴上Native层解析class的一段注释:

注释.png

这段注释的意思是,如果输入的optimzedDirectory为空,那么会使用默认的cache路径,也就是我们刚才提到的/data/dalvik-cache/。但是我们要注意到一点,一般情况下,我们的App对于这个文件夹是没有读写权限的,因此我们也就没有办法使用PathClassLoader去加载自定义的类。正如注释说的这个类一般由系统调用加载系统类和App应用。也就是说我们App中的类MainActivity等都是由其加载。

DexClassLoader

而我们如果要加载自定义的类应该使用DexClassLoader,也就是BaseDexClassLoader的另一个子类:

/**
 * 一个用于加载路径下指定的dex/jar/apk文件的ClasLoader
 * 可以加载沒有安装过的APK
 * 这个ClassLoader需要一个应用内私有,且可写入的路径去存储优化后的dex文件(optimizedDirectory)
 * 不要将优化后的文件存储在外部存储区,因为这将有可能导致你的App被恶意注入
 */
public class DexClassLoader extends BaseDexClassLoader {
    /**
     * @param dexPath 指定的dex/jar/apk文件的路径,可以包含多个路径,
     * 用{File.pathSeparator}分割。
     * @param optimizedDirectory 储存优化后文件的路径,必须是可写入的,不能为null
     * @param libraryPath 表示Native库路径
     * @param parent the parent class loader
     */
    public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}

可以看到,DexClassLoaderPathClassLoader类似,都只是重写了构造方法,我们可以看到,其实只是对于optimizedDirectory转换成File而已。这也真是他和PathClassLoader的不同之处,他可以自定义optimizedDirectory,我们可以指向一个我们有访问权限的文件,所以我们可以利用他来加载自定义的类。

总结

  1. Android重写了Java层的ClassLoader,延续了parent和双亲委派机制
  2. Android中同样的ClassLoader同样有一个最顶层的parent,不过不同于Java中用JNI实现,在Android中是Java实现的BootClassLoader,该类负责加载Android的核心类库
  3. BaseDexClassLoader,封装了一些列解析dex/jar/apk文件方法的基类。其在loadClass的时候会将dex文件解析并优化到optimizedDirectory路径下,再进行解析。
  4. PathClassLoader,其实他的位置有点想Java中的AppClassLoader,Android系统会通过他来加载系统应用类和App类。由于没有权限访问他的文件夹,所以不适用于我们加载自定义类,一般用于加载已经安装的Apk等
  5. DexClassLoader,用于加载自定义的类,包括没有安装过的APK也可以加载,需要指定optimizedDirectory来存储优化dex后的文件。
ClassLoader结构图

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 160,169评论 24 690
  • Android插件化基础的主要内容包括 Android插件化基础1-----加载SD上APKAndroid插件化基...
    隔壁老李头阅读 4,598评论 5 34
  • 从去年下半年开始,热修复技术在 Android 技术社区热了一阵子,这种不用发布新版本就可以修复线上 bug 的技...
    小小亭长阅读 5,223评论 6 19
  • Dalvik虚拟机如同其他Java虚拟机一样,在运行程序时首先需要将对应的类加载到内存中。而在Java标准的虚拟机...
    SilenceDut阅读 34,550评论 9 79
  • 城市建设的步伐越来越快,但是部分城市的建设还是和之前的模式差不多,硬件设施建设、招商,尽管在建设和招商前作了很好的...
    moreartedu阅读 323评论 0 0
  • 页面布局 这里以一道常考的面试题为例:假设高度已知,请写出三栏布局,其中左栏,右栏各位300px,中间自适应 这道...
    cAce阅读 146评论 0 0
  • 昏暗的天气,让人轻易悲伤,让人多愁善感。 时针逆转,我们来自不同的地方,不同的季节,不同的时间,但...
    Allisons阅读 434评论 1 2