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结构图
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容