Android類加載之PathClassLoader和DexClassLoader

北京的初雪.jpg

上一篇文章 自定義ClassLoader和雙親委派機制 講述了JVM中的類的加載機制,Android也是類JVM虛擬機那麼它的類加載機制是什麼呢,我們來探究一下(PS:文章源碼爲Android5.1)。

前言

Android的Dalvik虛擬機和Java虛擬機的運行原理相同都是將對應的java類加載在內存中運行。而Java虛擬機是加載class文件,也可以將一段二進制流通過defineClass方法生產Class進行加載(PS: 自定義ClassLoader和雙親委派機制 文章後面的自定義類加載器就是通過這種方式實現的)。Dalvik虛擬機加載的dex文件。dex文件是Android對與Class文件做的優化,以便於提高手機的性能。可以想象dex爲class文件的一個壓縮文件。dex在Android中的加載和class在jvm中的相同都是基於雙親委派模型,都是調用ClassLoader的loadClass方法加載類。

Android系統中類加載的雙親委派機制

  • Android5.1源碼中ClassLoader的loadClass方法
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;
            }
            //當所有父類節點的類加載器都沒有找到該類時,當前加載器調用findClass方法加載。
            if (clazz == null) {
                try {
                    clazz = findClass(className);
                } catch (ClassNotFoundException e) {
                    e.addSuppressed(suppressed);
                    throw e;
                }
            }
        }
  • 想要動態加載類,可以用 自定義ClassLoader和雙親委派機制 中自定義ClassLoader的方法加載自己定義的class文件麼?看看Android源碼中的ClassLoader的findClass方法:
protected Class<?> findClass(String className) throws ClassNotFoundException {
        throw new ClassNotFoundException(className);
    }

這個方法直接拋出了“ClassNotFoundException”異常,所以在Android中想通過這種方式實現類的加載時不行的。

Android系統中的類加載器

  • Android系統屏蔽了ClassLoader的findClass加載方法,那麼它自己的類加載時通過什麼樣的方式實現的呢?
    • Android系統中有兩個類加載器分別爲PathClassLoader和DexclassLoader。
    • PathClassLoader和DexClassLoader都是繼承與BaseDexClassLoader,BaseDexClassLoader繼承與ClassLoader。

提出問題

在這裏我們先提一個問題Android爲什麼會將自己的類加載器派生出兩個不同的子類,它們各自有什麼用?

BaseDexClassLoader類加載

  • 作爲ClassLoader的子類,複寫了父類的findClass方法。
@Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        //在自己的成員變量DexPathList中尋找,找不到拋異常
        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;
    }
  • DexPathList的findClass方法
public Class findClass(String name, List<Throwable> suppressed) {
        //循環便利成員變量dexElements,調用DexFile.loadClassBinaryName加載class
        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;
    }

通過以上兩段代碼我們可以看出,雖然Android中的ClassLoader的findClass方法的實現被取消了,但是ClassLoader的基類BaseDexClassLoader實現了findClass方法取加載指定的Class。

PathClassLoader和DexClassLoader比較

  • PathClassLoader
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);
    }
}
  • DexClassLoader
public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}
  • BaseDexClassLoader的構造函數
    /**
     * Constructs an instance.
     *
     * @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; may 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 BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }
  • dexPath:指定的是dex文件地址,多個地址可以用”:”進行分隔
    • optimizedDirectory:制定輸出dex優化後的odex文件,可以爲null
    • libraryPath:動態庫路徑(將被添加到app動態庫搜索路徑列表中)
    • parent:制定父類加載器,以保證雙親委派機制從而實現每個類只加載一次。

可以看出 PathClassLoader和DexClassLoader的區別就在於構造函數中optimizedDirectory這個參數。PathClassLoader中optimizedDirectory爲null,DexClassLoader中爲new File(optimizedDirectory)。

  • optimizedDirectory的幹活
    BaseDexClassLoader的構造函數利用optimizedDirectory創建了一個DexPathList,看看DexPathList中optimizedDirectory:
public DexPathList(ClassLoader definingContext, String dexPath,
        String libraryPath, File optimizedDirectory) {
    /******部分代碼省略******/
    this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                       suppressedExceptions);
    /******部分代碼省略******/
}
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,
                                         ArrayList<IOException> suppressedExceptions) {
   /******部分代碼省略******/
    for (File file : files) {
        /******部分代碼省略******/
        if (file.isDirectory()) {
           /******部分代碼省略******/
        } 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) {
                    suppressedExceptions.add(suppressed);
                }
            }
        } else {
            System.logW("ClassLoader referenced unknown path: " + file);
        }
        if ((zip != null) || (dex != null)) {
            elements.add(new Element(file, false, zip, dex));
        }
    }
    return elements.toArray(new Element[elements.size()]);
}

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不同生產的DexFile對象不同,我們繼續看看optimizedDirectory在DexFile中的作用:

public DexFile(File file) throws IOException {
    this(file.getPath());
}

/**
 * Opens a DEX file from a given filename. This will usually be a ZIP/JAR
 * file with a "classes.dex" inside.
 *
 * The VM will generate the name of the corresponding file in
 * /data/dalvik-cache and open it, possibly creating or updating
 * it first if system permissions allow.  Don't pass in the name of
 * a file in /data/dalvik-cache, as the named file is expected to be
 * in its original (pre-dexopt) state.
 *
 * @param fileName
 *            the filename of the DEX file
 *
 * @throws IOException
 *             if an I/O error occurs, such as the file not being found or
 *             access rights missing for opening it
 */
public DexFile(String fileName) throws IOException {
    mCookie = openDexFile(fileName, null, 0);
    mFileName = fileName;
    guard.open("close");
    //System.out.println("DEX FILE cookie is " + mCookie + " fileName=" + fileName);
}

/**
 * Opens a DEX file from a given filename, using a specified file
 * to hold the optimized data.
 *
 * @param sourceName
 *  Jar or APK file with "classes.dex".
 * @param outputName
 *  File that will hold the optimized form of the DEX data.
 * @param flags
 *  Enable optional features.
 */
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'll 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 + " sourceName=" + sourceName + " outputName=" + outputName);
}

static public DexFile loadDex(String sourcePathName, String outputPathName,
    int flags) throws IOException {
    return new DexFile(sourcePathName, outputPathName, flags);
}

從註釋當中就可以看到new DexFile(file)的dex輸出路徑只能爲/data/dalvik-cache,而DexFile.loadDex()的dex輸出路徑爲自己輸入的optimizedDirectory路徑。

dalvik-cache.jpg

解決疑問

我們在文章開始提出的問題就這樣一步步得到了答案。

DexClassLoader:能夠加載自定義的jar/apk/dex
PathClassLoader:只能加載系統中已經安裝過的apk
所以Android系統默認的類加載器爲PathClassLoader,而DexClassLoader可以像JVM的ClassLoader一樣提供動態加載。

總結

  • ClassLoader的loadClass方法保證了雙親委派機。
  • BaseDexClassLoader提供了兩種派生類使我們可以加載自定義類。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章