美文网首页Android开发经验·源码分析
Android类加载(三)——源码解读

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

作者: 程序员三千_ | 来源:发表于2020-03-16 20:44 被阅读0次

    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.
         *
         * <p>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:
         *
         * <ul>
         * <li>JAR/ZIP/APK files, possibly containing a "classes.dex" file as
         * well as arbitrary resources.
         * <li>Raw ".dex" files (not inside a zip file).
         * </ul>
         *
         * 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<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里我们也没有LoadClass的方法,所以我们直接去ClassLoader里去看,

    /**
         * Loads the class with the specified name. Invoking this method is
         * equivalent to calling {@code loadClass(className, false)}.
         * <p>
         * <strong>Note:</strong> In the Android reference implementation, the
         * second parameter of {@link #loadClass(String, boolean)} is ignored
         * anyway.
         * </p>
         *
         * @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:
         * <ol>
         * <li> Call {@link #findLoadedClass(String)} to determine if the requested
         * class has already been loaded.</li>
         * <li>If the class has not yet been loaded: Invoke this method on the
         * parent class loader.</li>
         * <li>If the class has still not been loaded: Call
         * {@link #findClass(String)} to find the class.</li>
         * </ol>
         * <p>
         * <strong>Note:</strong> In the Android reference implementation, the
         * {@code resolve} parameter is ignored; classes are never linked.
         * </p>
         *
         * @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<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;
        }
    
    

    在方法中看到,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<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;
                    }
                }
            }
            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<File> files, File optimizedDirectory,
                                                  List<IOException> suppressedExceptions) {
            List<Element> 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类加载(三)——源码解读

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