Android类加载(三)——源码解读

Android类加载(一)——DVM、ART、Dexopt、DexAot名词解析
Android类加载(二)——双亲委托机制
Android类加载(三)——源码解读

从上一篇文章我们知道,Android中类加载器的继承关系如下图:


ClassLoader相关类继承图

那么DexClassLoader和PathClassLoader是怎么实现类加载机制的呢?

public class DexClassLoader extends BaseDexClassLoader {
    /**
     * Creates a {@code DexClassLoader} that finds interpreted and native
     * code.  Interpreted classes are found in a set of DEX files contained
     * in Jar or APK files.
     *
     * 

The path lists are separated using the character specified by the * {@code path.separator} system property, which defaults to {@code :}. * * @param dexPath the list of jar/apk files containing classes and * resources, delimited by {@code File.pathSeparator}, which * defaults to {@code ":"} on Android * @param optimizedDirectory directory where optimized dex files * should be written; must not be {@code null} * @param libraryPath the list of directories containing native * libraries, delimited by {@code File.pathSeparator}; may be * {@code null} * @param parent the parent class loader */ public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) { super(dexPath, new File(optimizedDirectory), libraryPath, parent); } }

public class PathClassLoader extends BaseDexClassLoader {
    /**
     * Creates a {@code PathClassLoader} that operates on a given list of files
     * and directories. This method is equivalent to calling
     * {@link #PathClassLoader(String, String, ClassLoader)} with a
     * {@code null} value for the second argument (see description there).
     *
     * @param dexPath the list of jar/apk files containing classes and
     * resources, delimited by {@code File.pathSeparator}, which
     * defaults to {@code ":"} on Android
     * @param parent the parent class loader
     */
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }

    /**
     * Creates a {@code PathClassLoader} that operates on two given
     * lists of files and directories. The entries of the first list
     * should be one of the following:
     *
     * 
    *
  • JAR/ZIP/APK files, possibly containing a "classes.dex" file as * well as arbitrary resources. *
  • Raw ".dex" files (not inside a zip file). *
* * The entries of the second list should be directories containing * native library files. * * @param dexPath the list of jar/apk files containing classes and * resources, delimited by {@code File.pathSeparator}, which * defaults to {@code ":"} on Android * @param libraryPath the list of directories containing native * libraries, delimited by {@code File.pathSeparator}; may be * {@code null} * @param parent the parent class loader */ public PathClassLoader(String dexPath, String libraryPath, ClassLoader parent) { super(dexPath, null, libraryPath, parent); } }

在类DexClassLoader和类PathClassLoader,我们只看到了它们自身的构造方法,并没有真正去实现类加载的地方,所以,我们去看它们的父类BaseDexClassLoader

 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) 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里我们也没有LoadClass的方法,所以我们直接去ClassLoader里去看,

/**
     * Loads the class with the specified name. Invoking this method is
     * equivalent to calling {@code loadClass(className, false)}.
     * 

* Note: In the Android reference implementation, the * second parameter of {@link #loadClass(String, boolean)} is ignored * anyway. *

* * @return the {@code Class} object. * @param className * the name of the class to look for. * @throws ClassNotFoundException * if the class can not be found. */ public Class loadClass(String className) throws ClassNotFoundException { return loadClass(className, false); } /** * Loads the class with the specified name, optionally linking it after * loading. The following steps are performed: *
    *
  1. Call {@link #findLoadedClass(String)} to determine if the requested * class has already been loaded.
  2. *
  3. If the class has not yet been loaded: Invoke this method on the * parent class loader.
  4. *
  5. If the class has still not been loaded: Call * {@link #findClass(String)} to find the class.
  6. *
*

* Note: In the Android reference implementation, the * {@code resolve} parameter is ignored; classes are never linked. *

* * @return the {@code Class} object. * @param className * the name of the class to look for. * @param resolve * Indicates if the class should be resolved after loading. This * parameter is ignored on the Android reference implementation; * classes are not resolved. * @throws ClassNotFoundException * if the class can not be found. */ protected Class loadClass(String className, boolean resolve) throws ClassNotFoundException { Class clazz = findLoadedClass(className); if (clazz == null) { ClassNotFoundException suppressed = null; try { clazz = parent.loadClass(className, false); } catch (ClassNotFoundException e) { suppressed = e; } if (clazz == null) { try { clazz = findClass(className); } catch (ClassNotFoundException e) { e.addSuppressed(suppressed); throw e; } } } return clazz; }

从loadClass里我们可以看到,第一步先调用findLoadedClass去查找是否已经加载过class,如果先前加载过,直接返回对应的class,如果没找到,调用
parent.loadClass(className, false);,如果父亲找到了,直接返回,如果父亲没找到,就调用类自己的findClass,如果找到了就返回,没找到就报异常。(这段代码很好的解释了双亲委托机制)
我们先看findLoadedClass方法

/**
     * Returns the class with the specified name if it has already been loaded
     * by the VM or {@code null} if it has not yet been loaded.
     *
     * @param className
     *            the name of the class to look for.
     * @return the {@code Class} object or {@code null} if the requested class
     *         has not been loaded.
     */
    protected final Class findLoadedClass(String className) {
        ClassLoader loader;
        if (this == BootClassLoader.getInstance())
            loader = null;
        else
            loader = this;
        return VMClassLoader.findLoadedClass(loader, className);
    }

我们在点进VMClassLoader.findLoadedClass(loader, className);

native static Class findLoadedClass(ClassLoader cl, String name);

我们发现findLoadedClass是一个native方法,我们知道native是由C和C++实现的,大概我们也知道,就是把dex文件转化成class文件。我们只要知道被加载过的Class是有缓存的就OK了。我们再看parent.loadClass(className, false);它是一直递归调用parent的loadClass方法,直到找到或者找不到才停止。最后我们看到这个类自己的findClass方法,我们知道DexClassLoader和PathClassLoader都是派生于BaseDexClassLoader的,而且它们本身没有实现findClass方法,所以我们定位到BaseDexClassLoader里的findClass方法。

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

在方法中看到,Class c = pathList.findClass(name, suppressedExceptions);这段代码,我们点进pathList,发现pathList是BaseDexClassLoader的成员变量private final DexPathList pathList;
那么pathList是怎么生成的呢?我们在BaseDexClassLoader的构造方法里可以看到

 public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }

this.pathList是通过当前类的this,dexPath:dex目录,libraryPath:so库文件所在目录,optimizedDirectory:opt优化后dex所在的目录生成的。

那么pathList.findClass(name, suppressedExceptions);是怎么实现的呢?
所以我们找到DexPathList这个类的findClass方法。

 /**
     * Finds the named class in one of the dex files pointed at by
     * this instance. This will find the one in the earliest listed
     * path element. If the class is found but has not yet been
     * defined, then this method will define it in the defining
     * context that this instance was constructed with.
     *
     * @param name of class to find
     * @param suppressed exceptions encountered whilst finding the class
     * @return the named class or {@code null} if the class is not
     * found in any of the dex files
     */
    public Class findClass(String name, List suppressed) {
        for (Element element : dexElements) {
            DexFile dex = element.dexFile;

            if (dex != null) {
                Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
                if (clazz != null) {
                    return clazz;
                }
            }
        }
        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }

/**
     * Element of the dex/resource file path
     */
    /*package*/ static class Element {
        private final File dir;
        private final boolean isDirectory;
        private final File zip;
        private final DexFile dexFile;

        private ZipFile zipFile;
        private boolean initialized;

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

从这段代码我们可以看出,一开始先通过for循环Element数组,取出数组中每个Element对象(每个Element对象都包含一个DexFile对象),再调用Element的loadClassBinaryName方法获得每个Element对象所对应的class,如果找到了就返回,没找到就返回null,我们发现loadClassBinaryName是在DexFile类里,我们点进去看,最终是定位在是一个native方法上,所以每个Element对象所对应的class是在android底层通过C和C++代码实现的。那么Element数组是怎么创建出来的呢?

 public DexPathList(ClassLoader definingContext, String dexPath,
            String libraryPath, File optimizedDirectory) {

      ......
      .........
        this.dexElements = makePathElements(splitDexPath(dexPath), optimizedDirectory,suppressedExceptions);

我们发现Element数组是在DexPathList的构造方法里通过makePathElements方法创建的,所以我们进入makePathElements看看

 /**
     * Makes an array of dex/resource path elements, one per element of
     * the given array.
     */
    private static Element[] makePathElements(List files, File optimizedDirectory,
                                              List suppressedExceptions) {
        List elements = new ArrayList<>();
        /*
         * Open all files and load the (direct or contained) dex files
         * up front.
         */
        for (File file : files) {
            File zip = null;
            File dir = new File("");
            DexFile dex = null;
            String path = file.getPath();
            String name = file.getName();

            if (path.contains(zipSeparator)) {
                String split[] = path.split(zipSeparator, 2);
                zip = new File(split[0]);
                dir = new File(split[1]);
            } else if (file.isDirectory()) {
                // We support directories for looking up resources and native libraries.
                // Looking up resources in directories is useful for running libcore tests.
                elements.add(new Element(file, true, null, null));
            } else if (file.isFile()) {
                if (name.endsWith(DEX_SUFFIX)) {
                    // Raw dex file (not inside a zip/jar).
                    try {
                        dex = loadDexFile(file, optimizedDirectory);
                    } catch (IOException ex) {
                        System.logE("Unable to load dex file: " + file, ex);
                    }
                } else {
                    zip = file;

                    try {
                        dex = loadDexFile(file, optimizedDirectory);
                    } catch (IOException suppressed) {
                        /*
                         * IOException might get thrown "legitimately" by the DexFile constructor if
                         * the zip file turns out to be resource-only (that is, no classes.dex file
                         * in it).
                         * Let dex == null and hang on to the exception to add to the tea-leaves for
                         * when findClass returns null.
                         */
                        suppressedExceptions.add(suppressed);
                    }
                }
            } else {
                System.logW("ClassLoader referenced unknown path: " + file);
            }

            if ((zip != null) || (dex != null)) {
                elements.add(new Element(dir, false, zip, dex));
            }
        }

        return elements.toArray(new Element[elements.size()]);
    }

在代码中我们看,首先是for循环dex文件的数组,我们知道dex是文件,所以我们进入else if (file.isFile()) 这个判断里,我们再看到 if (name.endsWith(DEX_SUFFIX)) {,这个判断的意思就是如果name是DEX_SUFFIX 结尾的,进入这个判断,我们通过private static final String DEX_SUFFIX = ".dex";发现DEX_SUFFIX 就是.dex。所以这个判断就是如果是.dex结尾的文件进入这个判断,执行dex = loadDexFile(file, optimizedDirectory);这段代码,所以我们再进入loadDexFile方法

/**
     * Constructs a {@code DexFile} instance, as appropriate depending
     * on whether {@code optimizedDirectory} is {@code null}.
     */
    private static DexFile loadDexFile(File file, File optimizedDirectory)
            throws IOException {
        if (optimizedDirectory == null) {
            return new DexFile(file);
        } else {
            String optimizedPath = optimizedPathFor(file, optimizedDirectory);
            return DexFile.loadDex(file.getPath(), optimizedPath, 0);
        }
    }

loadDexFile最终是返回DexFile.loadDex,返回的是一个DexFile类型的文件。
所以说,在android中得到一个dex文件的地址,怎么把这个dex文件加载到虚拟机中来,就是通过DexFile来进行加载。其实在DexFile定义了很多native方法,最终加载就是通过这些native方法来实现的。

最终结论:ClassLoader最终是通过DexFile来实现的类加载,里面最重要的就是一个Element数组,这个数组中的每个Element包含一个DexFile对象,DexFile对象其实就代表一个要加载的dex文件。

其实热修复就是通过在Element数组前面插入新的dex文件来实现修复bug的。因为,在不同的dex文件中有相同的类存在时,那么会优先选择排在前面的dex文件中的类。所以,热修复时,会把有问题的类打包到一个dex文件中,然后把这个dex文件插入到Element数组的最前面。

你可能感兴趣的:(Android类加载(三)——源码解读)