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

作者: 芒果味的你呀 | 来源:发表于2018-10-10 19:23 被阅读34次
    文章大纲

    一. 类加载器

    Android中的类加载器中主要包括三类BootClassLoader,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. 什么样的类可以说是同一个类?

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

    相关文章

      网友评论

        本文标题:Android插件化之【类加载机制】

        本文链接:https://www.haomeiwen.com/subject/vdfjaftx.html