Android插件化之【类加载机制】

文章大纲

一. 类加载器

Android中的类加载器中主要包括三类BootClassLoader(继承ClassLoader),PathClassLoader和DexClassLoader。后两个继承于BaseDexClassLoader。
1.BootClassLoader:主要用于加载系统的类,包括java和android系统的类库。(比如TextView,Context,只要是系统的类都是由BootClassLoader加载完成)。

通过打印TextView.class.getClassLoader()即可验证

2.PathClassLoader:主要用于加载我们应用程序内的类。路径是固定的,只能加载
/data/app中的apk,无法指定解压释放dex的路径,无法动态加载。对于我们的应用默认为PathClassLoader

通过打印getClassLoader()以及ClassLoader.getSystemClassLoader()即可验证

3.DexClassLoader:可以用来加载任意路径的zip,jar或者apk文件。可以实现动态加载。

简单看一下这两个类的源码:

DexClassLoader类的源码如下:

package dalvik.system;
public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
//但是api26以上 这个函数源码如下 也就是第二个参数已经没有影响
// * @param optimizedDirectory this parameter is deprecated and has no effect since API level 26.
   public DexClassLoader(String dexPath, String optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }
}

PathClassLoader类的源码如下:

package dalvik.system;
public class PathClassLoader extends BaseDexClassLoader {
   public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }
    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }
}

参数意义
dexPath :需要被加载的jar/apk/dex 文件地址,可以多个,用File.pathSeparator分割。
optimizedDirectory:因为加载apk/jar的时候会被编译器优化解压出dex文件,这个路径就是保存dex文件的。但在api26以上这个参数默认也为null。
libraryPath:库lib文件的路径
parent:给DexClassLoader指定父加载器

可以发现PathClassLoader和DexClassLoader源码很简单,只包含了一个构造函数,去调用父类BaseDexClassLoader。(所有的工作都应该是在BaseDexClassLoader里完成的了。)而这两个加载器不同的是PathClassLoader的构造中少了optimizedDirectory这个参数,原因是PathClassLoader是加载/data/app中的apk,也就是系统中的apk,而这部分的apk都会解压释放dex到指定的目录:/data/dalvik-cache中,这个操作由系统完成,不需要单独传入路径,而DexClassLoader传入,用来缓存需要加载的dex文件,并创建一个DexFile对象,如果为null,会直接使用dex文件原有路径创建DexFile;这个参数已经弃用,自API26起无效;

二、 DexPathList

接下来具体看一下BaseDexClassLoader

public class BaseDexClassLoader extends ClassLoader {
    private final DexPathList pathList;
    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
    }
   @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
            for (Throwable t : suppressedExceptions) {
                cnfe.addSuppressed(t);
            }
            throw cnfe;
        }
        return c;
    }
}

在BaseDexClassLoader里我们可以看到根据传入的地址参数构造了一个DexPathList对象。从findClass方法可以看出来加载的类都是从pathList中查找。【findclass方法】是BaseDexClassLoader这个类的核心。那接下来看一下DexPathList类

private final Element[] dexElements;

public DexPathList(ClassLoader definingContext, String dexPath,
        String libraryPath, File optimizedDirectory) {
    ...
    this.definingContext = definingContext;
    this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,suppressedExceptions);
    ...
}

这里的重点是通过makeDexElements方法得到dexElements集合。而splitDexPath方法是将传入的文件集合转化为一个文件File合集,因为我们上面提到了dexPath可以是多个,用文件分隔符连接即可。

private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) {
    // 1.创建Element集合
    ArrayList<Element> elements = new ArrayList<Element>();
    // 2.遍历所有dex文件(也可能是jar、apk或zip文件)
    for (File file : files) {
        ZipFile zip = null;
        DexFile dex = null;
        String name = file.getName();
        ...
        // 如果是dex文件
        if (name.endsWith(DEX_SUFFIX)) {
            dex = loadDexFile(file, optimizedDirectory);

        // 如果是apk、jar、zip文件(这部分在不同的Android版本中,处理方式有细微差别)
        } else {
            zip = file;
            dex = loadDexFile(file, optimizedDirectory);
        }
        ...
        // 3.将dex文件或压缩文件包装成Element对象,并添加到Element集合中
        if ((zip != null) || (dex != null)) {
            elements.add(new Element(file, false, zip, dex));
        }
    }
    // 4.将Element集合转成Element数组返回
    return elements.toArray(new Element[elements.size()]);
}

总体来说,DexPathList的构造函数是将一个个的程序文件(可能是dex、apk、jar、zip)先通过loadDexFile转变成dex,然后封装成一个个Element对象,最后添加到Element集合中。BaseDexClassLoader的findclass方法也就是进一步,我们可以继续看DexPathList的findClass()方法了:

public Class findClass(String name, List<Throwable> suppressed) {
    for (Element element : dexElements) {
        // 遍历出一个dex文件
        DexFile dex = element.dexFile;

        if (dex != null) {
            // 在dex文件中查找类名与name相同的类
            Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }
    }
    if (dexElementsSuppressedExceptions != null) {
        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    }
    return null;
}

对Element数组进行遍历(也就是对每一个dex文件遍历),一个dex文件有很多类,通过调用DexFile的loadClassBinaryName找到与name相同的类返回,否则为null。正是这个特性!!我们可以把布丁dex作为Element数组的首个元素。这个就可以动态修复bug了!!【MultiDex方案以及由此衍生出的QQ空间热更新方案都是通过改变dexElements数组的元素位置来实现的】

结合图示
image.png

三、 双亲委派机制

如何理解Android ClassLoader的双亲代理/委派机制呢?ClassLoader的loadClass方法保证了双亲委派机制,那我们先看一下这个方法:

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

protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
        Class<?> clazz = findLoadedClass(className);//1
        if (clazz == null) {
            ClassNotFoundException suppressed = null;
            try {
                clazz = parent.loadClass(className, false);//2
            } catch (ClassNotFoundException e) {
                suppressed = e;
            }

            if (clazz == null) {
                try {
                    clazz = findClass(className);//3
                } catch (ClassNotFoundException e) {
                    e.addSuppressed(suppressed);
                    throw e;
                }
            }
        }
    return clazz;

1. 首先调用findLoadedClass看自身是否加载过该name的类文件。
2. 如果没有,调用父ClassLoader的loadClass看是否加载过类文件。
3. 如果父classLoader也没有加载过,表明我们这个类从来没有没加载过,则调用自身的findClass方法去dex文件中查找这个类。(联系我们上一节BaseDexClassLoader的findClass方法)

双亲委派模型的工作过程为:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的加载器都是如此,因此所有的类加载请求都会传给顶层的启动类加载器(BootClassLoader根加载器 加载器的顶端),只有当父加载器反馈自己无法完成该加载请求(该加载器的搜索范围中没有找到对应的类)时,子加载器才会尝试自己去加载。

总结说:
1. 什么是双亲委派机制:

ClassLoader在加载一个字节码时,首先会询问 当前的
ClassLoader是否已经加载过此类,如果已经加载过就直接返回,不在重复的去
加载,如果没有的话,会查询它的parent是否已经加载过此类,如果加载过那
么就直接返回parent加载过的字节码文件,如果整个继承线路上都没有加载过
此类,最后由子ClassLoader执行真正的加载。

2. 这样做的好处:

如果一个类被位于树中的任意ClassLoader节点加载过,就会缓存在内存里,那么在以后的整个系统的生命周期中这个类都不会在被重新加载,大大提高了加载类的效率。同样还能类隔离,防止其他类冒充系统类。

3. 什么样的类可以说是同一个类?

包名类名相同以及要被同一个类加载加载过。三个条件都满足,才能说是同一个类。

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

推荐阅读更多精彩内容