Android 插件化基础——ClassLoader 源码解析

其他有关插件化的文章欢迎大家观阅
插件化踩坑之路——Small和Atlas方案对比
Android插件化基础篇—— class 文件
Android插件化基础篇 — dex 文件
Android 插件化基础——虚拟机

Android 和 Java 平台的类加载平台区别较大,是我们基础篇的重点,我们将从三个方面来讲解 ClassLoader。

Java 中的 ClassLoader 回顾

Java平台的ClassLoader

之前的文章中,我们已经看过这张图了,那篇文章中也简单的讲解了类的加载流程,加载流程两个平台差不多,如何大家还不太熟悉可以去上面给出的虚拟机文章中再复习一下。

Android 中的 ClassLoader 详解

Android 中的 ClassLoader 种类

Android 中的 ClassLoader 有以下几种类型:

  • BootClassLoader
  • PathClassLoader
  • DexClassLoader
  • BaseDexClassLoader

BootClassLoader 作用和 Java 中的 Bootstrap ClassLoader 作用是类似的,是用来加载 Framework 层的字节码文件的。

PathClassLoader 作用和 Java 中的 App ClassLoader 作用有点类似,用来加载已经安装到系统中的 APK 文件中的 Class 文件。

DexClassLoader 和 Java 中的 Custom ClassLoader 作用类似,用来加载指定目录中的字节码文件。

BaseDexClassLoader 是一个父类,DexClassLoader 和 PathClassLoader 都是它的子类。

一个 App 至少需要 BootClassLoader 和 PathClassLoader 才能运行。为了证明这一点,我们写一个简单的页面,在 MainActivityonCreate() 方法中写下如下代码:

 ClassLoader classLoader = getClassLoader();
        if (classLoader != null) {
            Log.e("weaponzhi", "classLoader: " + classLoader.toString());

            while (classLoader.getParent() != null) {
                classLoader = classLoader.getParent();
                Log.e("weaponzhi","classLoader: "+classLoader.toString());
            }
        }

最后我们发现输出dalvik.system.PathClassLoaderjava.lang.BootClassLoader。当然不同机子可能输出的结果不同,但至少会有这两个 ClassLoader。BootClassLoader 负责加载 framework 字节码文件,所以每个应用都是需要的,而 PathClassLoader 用来加载已安装 Apk 的字节码文件,这些东西都是一个应用启动的必要东西。

Android 中 ClassLoader 特点及作用

Android 中的 ClassLoader 最大的特点就是双亲代理模型。双亲代理模型主要分三个过程:在加载字节码的时候,会询问当前 ClassLoader 是否已经加载过,如果加载过则直接返回,不再重复加载,如果没有的话,会查询 parent 是否加载过,如果加载过,就直接返回 parent 加载的字节码文件。如果整个继承线路上的 ClassLoader 都没有加载,执行类才会由当前 ClassLoader 类进行真正加载。

这样做的好处是,如果一个类被位于树中任意 ClassLoader 节点加载过,那么以后整个系统生命周期中,这个类都将不会被加载,大大提高了加载类的效率。由于这样的特点,就给我们 ClassLoader 带来了两个作用。

第一个作用就是类加载的共享功能。当一个 framework 层中的类被顶层 ClassLoader 加载过,那么这个类就会被缓存在内存里,以后任何需要用到底地方都不会重新加载了。

第二个作用就是类加载的隔离功能。不同继承路线上的 ClassLoader 加载的类肯定不是同一个类,这样就有一定的安全性,避免了用户自己写一些代码冒充核心类库来访问这些类库中核心代码和变量。

所以如何判断两个类是同一个类呢,不仅需要工程中的包名类名一致,还需要由同一个 ClassLoader 加载的,这三条同时满足才能说是一个类。

Android ClassLoader 源码讲解

我们下面就来通过源码来看看 Android ClassLoader 到底是如何实现双亲代理模式的。

首先我们进入 ClassLoader.java 这个类,查找它最核心的方法 loadClass() 看看它是怎么实现的

 protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // 1.查看 class 是否已经被加载过
            Class c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
            //2.如果没有被加载过,则判断 parent ClassLoader 有没有加载过
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
            //3.如果类没有被加载过,那么就通过当前 ClassLoader 来加载
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                }
            }
            return c;
    }

我在代码中注释已经比较清楚了,源码中首先会判断当前的 ClassLoader 有没有加载过这个类,如果没有加载过,再会看看 parent ClassLoader 有没有加载过,如果整个继承线路走过后 class 依然为 null,则再回到当前 ClassLoader 通过 findClass() 方法来加载 class。

好,现在让我们继续跟踪 findClass()方法,进去后发现这个方法是个空实现,说明真正的实现代码都在 ClassLoader 的子类中实现,我们在 Android Studio 中,查找类似 PathClassLoader 这样的类是无法看到代码的,所以我们可以通过源码网站 AndroidXRef 或者其他观看源码的方式来查看下 Android 几个 ClassLoader 的具体实现。

打开 DexClassLoader发现很简单,类中只有一个构造方法,继承自 BaseDexClassLoader,下面我们来看看这个构造方法。

public DexClassLoader(String dexPath, String optimizedDirectory,String libraryPath,ClassLoader parent) {
    super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}

参数dexpath指定我们要加载的 dex 文件路径,optimizedDirectory指定该 dex 文件要被拷贝到哪个路径中,一般是应用程序内部路径。

DexClassLoader类上有一段官方注释:

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.

这段注释的意思就是,DexClassLoader 可以加载一些 jar 包和 apk 包里面的 dex 文件,可以用来加载一些并没有安装到系统应用中的类。所以,DexClassLoader 是动态加载的核心

下面我们再来看看 PathClassLoader 是如何实现的,它同样也是继承于 BaseDexClassLoader,并且也重写了构造方法。

public PathClassLoader(String dexPath, String libraryPath,ClassLoader parent) {
    super(dexPath, null, libraryPath, parent);
}

我们可以看到,它和 DexClassLoader 的区别就在于少了一个 optimizedDirectory 的参数,所以 PathClassLoader 没有办法加载没有安装到系统中的应用的类。

我们发现,这两个 ClassLoader 并没有什么具体实现,真正的实现都是在他们的父类 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) throw ClassNotFoundException{
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name,suppressedExceptions);
        if (c == null){
            ClassNotFoundException cnfe = new ClassNorFoundException("xxx");
            for (Throwable t : suppressedExceptions){
                cnfe.addSuppressed(t);
            }
            throw cnfe;
        }
        return c;
    }
}

我们通过构造方法可以观察到,如果 optimizedDirectory 为空,那么代表这是 PathClassLoader,不为空则是 DexClassLoader,findClass()方法虽然我们终于看到了实现,但发现真正的实现还没有在这里,而是在 DexPathList对象的findClass()方法中,不要气馁,结果就在前方,我们继续跟进!

DexPathList这个类代码比较多,我们来从它的成员变量中开始,挑重点看。

final class DexPathList{
    private static final String DEX_SUFFIX = ".dex";
    private final ClassLoader definingContext;
    private final Element[] dexElements;
    ...
    public DexPathList(ClassLoader definingContext,
        String dexPath,String libraryPath,File optimizedDirectory){
        ...
        this.dexElements = makeDexElements(splitDexPath(dexPath),optimizedDirectory,suppressedException);
        ...
    }

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

我们关注几个点,一个是 DEX_SUFFIX 这个成员变量,代表 dex 文件后缀,方便后面的一些文件处理判断使用。 definingContext 就是在初始化的时候传进来的 ClassLoader,dexElements DexPathList 中一个静态内部类对象数组,在构造方法中初始化,这个对象数组是 findClass() 的关键参数,通过遍历获取 Elements 中的 DexFile 对象,调用 DexFile 的 loadClassBinaryName() 方法,完成 class 文件的获取。

static class Element{
    private final File file;
    private final boolean isDirectory;
    private final File zip;
    private final DexFile dexFile;

    public Element(File file,boolean isDirectory,File zip,DexFile dexFile){
        this.dir = dir;
        this.isDirectory = isDirectory;
        this.zip = zip;
        this.dexFIle = dexFIle;
    }
}

Element 就是 dexElements 对象数组存储的具体静态内部类,该类我只是简单列举下它的成员变量。dexElements 在 DexPathList 的构造方法中初始化,我们来细致的看下 makeDexElements 方法,该方法直接指向 makeElements()方法,源码如下:

private static Element[] makeElements(List<File> files,File optimizedDirectory,
                                      List<IOException> suppressedExceptions,
                                      boolean ignoreDexFiles,
                                      ClassLoader loader){
        Element[] elements = new Element[file.size()];
        int elementsPos = 0;
        for (File file : files){
            File zip = null;
            File dir = new File("");
            DexFile dex = null;
            String path = file.getPath();
            String name = file.getName();
            //1
            if (path.contains(zipSeparator)){
                ...
            //2
            }else if(file.isDirectory()){
                elements[elementsPos++] == new Element(file,true,null,null);
            //3
            }else if (file.isFile()){
                //4
                if(!ignoreDexFiles && name.endsWith(DEX_SUFFIX)){
                    dex = loadDexFile(file,optimizedDirectory,loader,elements);
                //5
                }else{
                    zip = file;
                    //6
                    if(!ignoreDexFiles){
                        dex = loadDexFile(file,optimizedDirectory,loader,elements);
                    }
                }
            }
        }                                     
}

这里我省略掉了一些代码,只看重点。其中注释中第一个和第二个 if 语句中的代码的作用是如果路径是文件夹的话,就继续向下递归,第三个判断是否是文件,如果是,进入第四个,判断文件是否是以 .dex 为后缀的,如果是的话标明这个文件就是我们需要加载的 dex 文件,通过 loadDexFile() 方法来加载 DexFile 对象。如果是文件,并且是个压缩文件的话,就会进入第五个 if 语句中,同样会通过 loadDexFile() 来进行 DexFile 加载。下面来看一下 loadDexFile() 方法实现。

private static DexFile loadDexFile(File file,File optimizedDirectory,Classloader loader,
                                   Element[] elements) throw IOException{
        if(optimizedDirectory == null){
            return new DexFile(file,loader,elements);
        }else{
            String optimizedPath = optimizedPathFor(file,optimizedDirectory);
        }
}

如果optimizedDirectory为空,说明文件就是 dex 文件,那么直接创建 DexFile 对象即可,如果不为空,则调用 loadDex() 方法,将它解压然后获取内部真正的 DexFile。所以 makeElements() 就是通过文件获取 dex 文件,转化为 Elements 对象数组,然后给findClass() 方法使用。

loadClassBinaryName()方法再往下走就是 native 方法了,我们就无法继续看了,大概可以想像这个 native 方法就是通过 C、C++去查找 dex 指定 name 相关的东西,然后将它拼成 class 字节码,最后返回给我们。

整体的源码我们大概就看过了,实际上不是很复杂,只是嵌套很多,真正复杂的地方都在 native 中了,所以我们看源码一定要耐心细心,不能惧怕,看不懂就多看几遍,学习一下他们的编程思路和设计思想,对我们能力提高有极大帮助。

Android 中的动态加载比 Java 程序复杂在哪里

Android 中的动态加载在我们之前源码分析之后,感觉看起来不是很复杂,只要利用好几个 ClassLoader ,整体的思路还是比较清晰的,但在实际设计的时候远远没有那么简单,主要是因为 Android 有他的复杂性:

  • 有许多组件类,比如四大组件,都是需要注册才能使用的。需要在 AndoridManifest 注册才能使用。
  • 资源的动态加载非常复杂。Android 的资源很特殊,都是通过 id 注册的,通过 id 从 Resource 实例中获取对应的资源,如果是动态加载的新类,资源 id 就会找不到,总而言之就是资源也是需要动态注册的
  • Android 每个版本对于类和资源加载的方式都是不同的,适配也是一个极为头疼的问题。

以上难点总结起来可以用一句话概括:「Android 程序运行需要一个上下文环境」。上下文环境可以给组件提供需要的功能,比如主题、资源、查询组件等。那么我们如何给动态加载的组件和类提供上下文环境呢,其实这就是第三方动态加载库主要解决的问题,也是非常复杂的,像 Tinker 和 Atlas 这些比较成熟的动态加载方案都是以解决这些问题作为核心而设计的,我们个人要解决可能比较困难,但我们可以通过使用和阅读源码,来学习他们的实现原理,大致了解即可。


下一篇文章我们将利用我们学到的 ClassLoader 相关知识,自己尝试写一个简单的插件加载 demo 和插件管理器。

本文部分内容参考于慕课网实战课程「Android 应用发展趋势必备武器 热修复与插件化」,有兴趣的朋友可以付费学习。
插件化实战课程

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

推荐阅读更多精彩内容