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

Android插件化之【类加载机制】_第1张图片
文章大纲

一. 类加载器

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 suppressedExceptions = new ArrayList();
        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 files, File optimizedDirectory, ArrayList suppressedExceptions) {
    // 1.创建Element集合
    ArrayList elements = new ArrayList();
    // 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 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数组的元素位置来实现的】

结合图示
Android插件化之【类加载机制】_第2张图片
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插件化之【类加载机制】)