android Dex文件的加载

上篇文章讲到了apk的分包,通过multidex构建出包含多个dex文件的apk,从而解决65536的方法数限制问题《Android Dex分包》。

在dalvik虚拟机上,应用启动时只会加载主dex文件,而从dex需要我们手动去加载,那么问题来了,如何手动加载一个dex文件?前面也提到了,使用DexClassLoader和PathClassLoader。

DexClassLoader和PathClassLoader

android加载dex、jar、apk主要是通过DexClassLoader或者PathClassLoader来实现

下面先看一下DexClassLoader的实现

/**
 * A class loader that loads classes from {@code .jar} and {@code .apk} files
 * containing a {@code classes.dex} entry. This can be used to execute code not
 * installed as part of an application.
 *
 * 

This class loader requires an application-private, writable directory to * cache optimized classes. Use {@code Context.getDir(String, int)} to create * such a directory:

   {@code
 *   File dexOutputDir = context.getDir("dex", 0);
 * }
* *

Do not cache optimized classes on external storage. * External storage does not provide access controls necessary to protect your * application from code injection attacks. */ 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); } }

代码很简单,只有一个构造方法,调用了父类的构造方法,
参数dexPath为dex、jar、apk文件的路径,多个路径之间用:分隔
optimizedDirectory: dex文件首次加载时会进行dexopt操作,optimizedDirectory即为优化后的odex文件的存放目录,不允许为空,官方推荐使用应用私有目录来缓存优化后的dex文件,dexOutputDir = context.getDir("dex", 0);
libraryPath:动态库的路径,可以为空
parent:ClassLoader类型的参数,当前类加载器的父加载器

再来看看PathClassLoader的源码实现

/**
 * Provides a simple {@link ClassLoader} implementation that operates on a list
 * of files and directories in the local file system, but does not attempt to
 * load classes from the network. Android uses this class for its system class
 * loader and for its application class loader(s).
 */
public class PathClassLoader extends BaseDexClassLoader {

    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }

    public PathClassLoader(String dexPath, String libraryPath,
            ClassLoader parent) {
        super(dexPath, null, libraryPath, parent);
    }
}


可以看到android系统采用PathClassLoader作为其系统加载器以及应用加载器.PathClassLoader 和DexClassLoader的区别就在于optimizedDirectory参数是否为空,关于optimizedDirectory的作用,接着往下看.

DexClassLoader、PathClassLoader都是继承自BaseDexClassLoader,而BaseDexClassLoader又继承自ClassLoader

/**
 * Base class for common functionality between various dex-based
 * {@link ClassLoader} implementations.
 */
public class BaseDexClassLoader extends ClassLoader {
    private final DexPathList pathList;

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

    //省略其他代码...
}

BaseDexClassLoader继承自ClassLoader,构造方法中先调用父类ClassLoader的构造方法,然后初始化了DexPathList对象,再来看看DexPathList的构造方法


    public DexPathList(ClassLoader definingContext, String dexPath,
            String libraryPath, File optimizedDirectory) {
        if (definingContext == null) {
            throw new NullPointerException("definingContext == null");
        }

        if (dexPath == null) {
            throw new NullPointerException("dexPath == null");
        }

        if (optimizedDirectory != null) {
            if (!optimizedDirectory.exists())  {
                throw new IllegalArgumentException(
                        "optimizedDirectory doesn't exist: "
                        + optimizedDirectory);
            }

            if (!(optimizedDirectory.canRead()
                            && optimizedDirectory.canWrite())) {
                throw new IllegalArgumentException(
                        "optimizedDirectory not readable/writable: "
                        + optimizedDirectory);
            }
        }

        this.definingContext = definingContext;
        ArrayList suppressedExceptions = new ArrayList();
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions);
        if (suppressedExceptions.size() > 0) {
            this.dexElementsSuppressedExceptions =
                suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
        } else {
            dexElementsSuppressedExceptions = null;
        }
        this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
    }

DexPathList的构造方法先对传入的参数进行校验,然后调用makeDexElements解析出dex相关参数,并保存到dexElements成员变量中,再来看makeDexElements方法

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

            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 if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
                    || name.endsWith(ZIP_SUFFIX)) {
                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 if (file.isDirectory()) {
                // We support directories for looking up resources.
                // This is only useful for running libcore tests.
                elements.add(new Element(file, true, null, null));
            } else {
                System.logW("Unknown file type for: " + file);
            }

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

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

files为dex文件的file对象list,判断是dex文件之后调用loadDexFile方法加载dex文件,返回DexFile对象。

  /**
     * 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);
        }
    }

该方法对optimizedDirectory参数有区分处理,即DexClassLoader和PathClassLoader的区别就在这,若optimizedDirectory为空(采用PathClassLoader),直接返回DexdFile对象,若不为空(采用DexClassLoader),则先调用optimizedPathFor方法获取dex文件优化后存放的目录,如果不是dex文件则将后缀替换为 .dex结尾,最后又调用了DexFile.loadDex静态方法返回了DexFile对象。

 /**
     * Converts a dex/jar file path and an output directory to an
     * output file path for an associated optimized dex file.
     */
    private static String optimizedPathFor(File path,
            File optimizedDirectory) {

        String fileName = path.getName();
        if (!fileName.endsWith(DEX_SUFFIX)) {
            int lastDot = fileName.lastIndexOf(".");
            if (lastDot < 0) {
                fileName += DEX_SUFFIX;
            } else {
                StringBuilder sb = new StringBuilder(lastDot + 4);
                sb.append(fileName, 0, lastDot);
                sb.append(DEX_SUFFIX);
                fileName = sb.toString();
            }
        }

        File result = new File(optimizedDirectory, fileName);
        return result.getPath();
    }
    static public DexFile loadDex(String sourcePathName, String outputPathName,
        int flags) throws IOException {
        /*
         * TODO: we may want to cache previously-opened DexFile objects.
         * The cache would be synchronized with close().  This would help
         * us avoid mapping the same DEX more than once when an app
         * decided to open it multiple times.  In practice this may not
         * be a real issue.
         */
        return new DexFile(sourcePathName, outputPathName, flags);
    }

可以看到DexFile.loadDex方法直接调用了DexFile的构造方法

    //PathClassLoader调用
  public DexFile(File file) throws IOException {
        this(file.getPath());
  }

  public DexFile(String fileName) throws IOException {
     mCookie = openDexFile(fileName, null, 0);
     mFileName = fileName;
     guard.open("close");
     //System.out.println("DEX FILE cookie is " + mCookie);
 }

   //DexClassLoader调用
 private DexFile(String sourceName, String outputName, int flags) throws IOException {
     if (outputName != null) {
         try {
             String parent = new File(outputName).getParent();
             if (Libcore.os.getuid() != Libcore.os.stat(parent).st_uid) {
                 throw new IllegalArgumentException("Optimized data directory " + parent
                         + " is not owned by the current user. Shared storage cannot protect"
                         + " your application from code injection attacks.");
             }
         } catch (ErrnoException ignored) {
             // assume we will fail with a more contextual error later
         }
     }
     mCookie = openDexFile(sourceName, outputName, flags);
     mFileName = sourceName;
     guard.open("close");
     //System.out.println("DEX FILE cookie is " + mCookie);
    }

所以BaseDexClassLoader和PathClassLoader最终都是调用了openDexFile方法,唯一的区别就是outputName是否为空,即优化后dex保存的路径,

/*
     * Open a DEX file.  The value returned is a magic VM cookie.  On
     * failure, an IOException is thrown.
     */
    private static int openDexFile(String sourceName, String outputName,
        int flags) throws IOException {
        return openDexFileNative(new File(sourceName).getCanonicalPath(),
                                 (outputName == null) ? null : new File(outputName).getCanonicalPath(),
                                 flags);
    }

    native private static int openDexFileNative(String sourceName, String outputName,
        int flags) throws IOException;

在native方法中对其进行了判断,如果outputName为空,则自动生成一个缓存目录,即/data/dalvik-cache/[email protected]。所以DexClassLoader与PathClassLoader的本质区别,就是DexClassLoader可以指定odex的路径,而PathClassLoader则采用系统默认的缓存路径。
所以一般PathDexClassLoader只能加载已安装的apk的dex,而DexClassLoader则可以加载指定路径的apk、dex和jar,也可以从sd卡中进行加载。

openDexFileNative代码中主要是对dex文件进行了优化操作,并将优将优化后得dex文件(odex文件)通过mmap映射到内存中。

其中的细节可以参考《DexClassLoader和PathClassLoader加载Dex流程》一文

类的加载

上述我们得到DexClassLoader或者PathClassLoader对象后,就可以调用其loadClass方法来动态加载某个类
DexClassLoader、PathClassLoader以及BaseDexClassLoader都没有实现这个方法,接着再去其父类ClassLoader中找,

  /**
     * 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; }

首先调用了findLoadedClass查找当前虚拟机是否已经加载过该类,是则直接返回该class,如果未加载过,则调用父加载器的loadClass方法,

这里采用了java的双亲委派模型,即当一个加载器被请求加载某个类时,它首先委托自己的父加载器去加载,一直向上查找,若顶级加载器(优先)或父类加载器能加载,则返回这个类所对应的Class对象,若不能加载,则最后再由请求发起者去加载该类。这种方式的优点就是能够保证类的加载按照一定的规则次序进行,越是基础的类,越是被上层的类加载器进行加载,从而保证程序的安全性。

  /**
     * 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);
    }

上述由于该类还未加载,所以findLoadedClass会返回null,所以会调用parent.loadClass,而DexClassLoader在使用时一般采用默认的类加载器作为其父类加载器

DexClassLoader dexClassLoader = new DexClassLoader(dexPath, getDir("dex", 0).getAbsolutePath(), dexPath, getClassLoader());

即直接调用Context的getClassLoader方法,该方法为抽象方法,实现是在ContextImpl中


//ContextImpl.java
  @Override
    public ClassLoader getClassLoader() {
        return mPackageInfo != null ?
                mPackageInfo.getClassLoader() : ClassLoader.getSystemClassLoader();
    }
    
//ClassLoader.java
 /**
     * Returns the system class loader. This is the parent for new
     * {@code ClassLoader} instances and is typically the class loader used to
     * start the application.
     */
    public static ClassLoader getSystemClassLoader() {
        return SystemClassLoader.loader;
    }
    
    //SystemClassLoader为ClassLoader的内部类
     static private class SystemClassLoader {
        public static ClassLoader loader = ClassLoader.createSystemClassLoader();
    }
    
     /**
     * Create the system class loader. Note this is NOT the bootstrap class
     * loader (which is managed by the VM). We use a null value for the parent
     * to indicate that the bootstrap loader is our parent.
     */
    private static ClassLoader createSystemClassLoader() {
        String classPath = System.getProperty("java.class.path", ".");
        //最终返回了PathClassLoader作为系统加载器SystemClassLoader,而其父类为根加载器BootClassLoader
        return new PathClassLoader(classPath, BootClassLoader.getInstance());
    }

所以ClassLoader的loadClass最终会调用根加载器BootClassLoader的loadClass方法,BootClassLoader也是ClassLoader的内部类,是android平台上所有ClassLoader的parent,其loadClass也是先调用findLoadedClass, 这里未加载过直接返回null,根加载器已经是顶级加载器,所以这里直接调用了findClass方法

  @Override
    protected Class loadClass(String className, boolean resolve)
           throws ClassNotFoundException {
        Class clazz = findLoadedClass(className);

        if (clazz == null) {
            clazz = findClass(className);
        }

        return clazz;
    }
    
    @Override
    protected Class findClass(String name) throws ClassNotFoundException {
        return Class.classForName(name, false, null);
    }

findClass方法也是返回Class.classForName,这里第三个参数为null,采用的是根加载器,而根加载器是用来加载java核心类,无法加载用户定义的类,所以这里返回为空

所以又回到一开始ClassLoader的loadClass方法,调用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;
    }

可以看到findClass直接调用pathList的findClass方法,如果为空,抛出ClassNotFoundExceptioin异常,如果不为空,则直接返回该Class

pathList即BaseDexClassLoader中的DexPathList成员变量,其中保存了dexFile的Elements集合,

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

DexPathList的findClass遍历dexElements对象,并调用dexFile的loadClassBinaryName native方法来加载Class.

所以之前在dex分包的时候,我们通过PathClassLoader获取已加载的保存在pathList中的dex信息,然后利用DexClassLoadder加载我们指定的从dex文件,将dex信息合并到pathList的dexElements中,从而在app运行的时候能够将所有的dex中的类加载到内存中。

参考文章

DexClassLoader源码
《DexClassLoader和PathClassLoader加载Dex流程》
《Android动态加载——DexClassloader分析》

http://androidxref.com/4.4.2_r1/xref/libcore/dalvik/src/main/java/dalvik/system/DexClassLoader.java

你可能感兴趣的:(android Dex文件的加载)