Android類加載之PathClassLoader和DexClassLoader

北京的初雪.jpg

第二次修改

任何結(jié)論在沒有經(jīng)過實(shí)際檢驗(yàn)之前都不能夠確定一定沒問題。三年前寫的文章回過頭來發(fā)現(xiàn)有些部分內(nèi)容是有問題的(PS:這的確比較尷尬),再次對(duì)結(jié)果進(jìn)行驗(yàn)證之后重新修改了之前的結(jié)論。幸虧文章閱讀量不是很多,希望被誤導(dǎo)的同學(xué)能夠在其他地方得到正確結(jié)論。


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

前言

AndroidDalvik 虛擬機(jī)和 Java 虛擬機(jī)的運(yùn)行原理相同都是將對(duì)應(yīng)的 java 類加載在內(nèi)存中運(yùn)行。而 Java 虛擬機(jī)是加載 class 文件,也可以將一段二進(jìn)制流通過 defineClass 方法生產(chǎn) Class 進(jìn)行加載(PS: 自定義ClassLoader和雙親委派機(jī)制 文章后面的自定義類加載器就是通過這種方式實(shí)現(xiàn)的)。Dalvik 虛擬機(jī)加載的 dex 文件。dex 文件是 Android 對(duì)與 Class 文件做的優(yōu)化,以便于提高手機(jī)的性能。可以想象 dexclass 文件的一個(gè)壓縮文件。dexAndroid 中的加載和 classjvm 中的相同都是基于雙親委派模型,都是調(diào)用ClassLoaderloadClass 方法加載類。

Android系統(tǒng)中類加載的雙親委派機(jī)制

  • Android5.1 源碼中 ClassLoaderloadClass 方法
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;
            }
            //當(dāng)所有父類節(jié)點(diǎn)的類加載器都沒有找到該類時(shí),當(dāng)前加載器調(diào)用findClass方法加載。
            if (clazz == null) {
                try {
                    clazz = findClass(className);
                } catch (ClassNotFoundException e) {
                    e.addSuppressed(suppressed);
                    throw e;
                }
            }
        }
  • 想要?jiǎng)討B(tài)加載類,可以用 自定義ClassLoader和雙親委派機(jī)制 中自定義 ClassLoader 的方法加載自己定義的 class 文件么?看看 Android 源碼中的ClassLoaderfindClass 方法:
protected Class<?> findClass(String className) throws ClassNotFoundException {
        throw new ClassNotFoundException(className);
}

這個(gè)方法直接拋出了 ClassNotFoundException 異常,所以在 Android 中想通過這種方式實(shí)現(xiàn)類的加載時(shí)不行的。

Android系統(tǒng)中的類加載器

  • Android 系統(tǒng)屏蔽了 ClassLoaderfindClass 加載方法,那么它自己的類加載時(shí)通過什么樣的方式實(shí)現(xiàn)的呢?
  • Android 系統(tǒng)中有兩個(gè)類加載器分別為 PathClassLoaderDexclassLoader
  • PathClassLoaderDexClassLoader 都是繼承與BaseDexClassLoaderBaseDexClassLoader 繼承與 ClassLoader

提出問題

在這里我們先提一個(gè)問題 Android 為什么會(huì)將自己的類加載器派生出兩個(gè)不同的子類,它們各自有什么用?

BaseDexClassLoader類加載

  • 作為 ClassLoader 的子類,復(fù)寫了父類的 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;
    }
  • DexPathListfindClass 方法
public Class findClass(String name, List<Throwable> suppressed) {
        //循環(huán)便利成員變量dexElements,調(diào)用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 中的 ClassLoaderfindClass 方法的實(shí)現(xiàn)被取消了,但是 ClassLoader 的基類 BaseDexClassLoader 實(shí)現(xiàn)了 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 的構(gòu)造函數(shù)
    /**
     * 構(gòu)造方法
     * @param dexPath 包含類和資源的jar/apk文件列表,Android中使用“:”拆分
     * @param optimizedDirectory 優(yōu)化的dex文件所在的目錄,可以為空;
     * @param libraryPath 動(dòng)態(tài)庫路徑,可以為空;
     * @param parent 父類加載器
     */
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }
  • dexPath:指定的是dex文件地址,多個(gè)地址可以用":"進(jìn)行分隔
  • optimizedDirectory:制定輸出 dex 優(yōu)化后的 odex 文件,可以為null
  • libraryPath :動(dòng)態(tài)庫路徑(將被添加到 app 動(dòng)態(tài)庫搜索路徑列表中)
  • parent :制定父類加載器,以保證雙親委派機(jī)制從而實(shí)現(xiàn)每個(gè)類只加載一次。

可以看出 PathClassLoaderDexClassLoader 的區(qū)別就在于構(gòu)造函數(shù)中 optimizedDirectory 這個(gè)參數(shù)。PathClassLoaderoptimizedDirectorynullDexClassLoader 中為 new File(optimizedDirectory)

optimizedDirectory 的作用

BaseDexClassLoader 的構(gòu)造函數(shù)利用 optimizedDirectory 創(chuàng)建了一個(gè)DexPathList,看看 DexPathListoptimizedDirectory:

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 不同生產(chǎn)的 DexFile 對(duì)象不同,我們繼續(xù)看看 optimizedDirectoryDexFile 中的作用:

public DexFile(File file) throws IOException {
    this(file.getPath());
}
/**
 * 從給定的File對(duì)象打開一個(gè)DEX文件。這通常是一個(gè)ZIP/JAR 文件,其中包含“classes.dex”。
 * VM將在/data/dalvik-cache中生成相應(yīng)文件的名稱,然后打開它,如果允許系統(tǒng)權(quán)限,可能會(huì)首先創(chuàng)建或更新它。不要在/data/dalvik-cache中傳遞文件名,因?yàn)轭A(yù)期該命名文件為原始狀態(tài)(pre-dexopt)。
 * @param fileName 引用實(shí)際DEX文件的File對(duì)象
 * @throws IOException 找不到文件或*打開文件缺少訪問權(quán)限,回拋出io異常
 */
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);
}

/**
 * 打開一個(gè)DEX文件。返回的值是VM cookie
 * @param sourceName Jar或APK文件包含“ classes.dex”。
 * @param outputName 包含優(yōu)化形式的DEX數(shù)據(jù)的文件。
 * @param flags 啟用可選功能。
 */
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);
}

//打開dex的native方法,/art/runtime/native/dalvik_system_DexFile.cc
private static native long openDexFileNative(String sourceName, String outputName, int flags);
//打開一個(gè)DEX文件。返回的值是VM cookie。 失敗時(shí),將引發(fā)IOException。
private static long openDexFile(String sourceName, String outputName, int flags) throws IOException {
    // Use absolute paths to enable the use of relative paths when testing on host.
    return openDexFileNative(new File(sourceName).getAbsolutePath(),
                                (outputName == null) ? null : new File(outputName).getAbsolutePath(),
                                flags);
}

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

dalvik-cache.jpg

解決疑問

我們?cè)谖恼麻_始提出的問題就這樣一步步得到了答案。

  • DexClassLoader:能夠加載 jar/apk/dex ,也可以指定對(duì) dex 優(yōu)化后的 odex 的輸出文件目錄;
  • PathClassLoader:只能夠加載 jar/apk/dex ,但它的 optimizedDirectory 沒法自己設(shè)定;

所以 Android 系統(tǒng)默認(rèn)的類加載器為 PathClassLoader,而DexClassLoader 可以像 JVMClassLoader 一樣提供動(dòng)態(tài)加載。

android 8.0上的修改

android-26版本的BaseDexClassLoader.java

/**
 * Constructs an instance.
 * Note that all the *.jar and *.apk files from {@code dexPath} might be
 * first extracted in-memory before the code is loaded. This can be avoided
 * by passing raw dex files (*.dex) in the {@code dexPath}.
 *
 * @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 this parameter is deprecated and has no effect
 * @param librarySearchPath 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 librarySearchPath, ClassLoader parent) {
    super(parent);
    this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);

    if (reporter != null) {
        reporter.report(this.pathList.getDexPaths());
    }
}

重點(diǎn)關(guān)注 @param optimizedDirectory this parameter is deprecated and has no effect ,表明在 Android26optimizedDirectory 已經(jīng)被棄用。
所以在 Android26 之后 PathClassLoaderDexClassLoader 已經(jīng)沒有了區(qū)別。

從JVM到Dalivk再到ART(class,dex,odex,vdex,ELF) 這篇文章中講述了 Android26 中其他 dex 相關(guān)的優(yōu)化。

總結(jié)

  • ClassLoaderloadClass 方法保證了雙親委派機(jī)。
  • BaseDexClassLoader 提供了兩種派生類使我們可以加載自定義類。

另外還有一個(gè)問題自己沒太搞清楚,默認(rèn)的optimizedDirectory 是哪個(gè)路徑?
data/appdata/dalvik-cache 下面都沒有我加載的外部的 dex 文件,有誰找到了結(jié)果可以分享一下。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容