
第二次修改
任何結(jié)論在沒有經(jīng)過實際檢驗之前都不能夠確定一定沒問題。三年前寫的文章回過頭來發(fā)現(xiàn)有些部分內(nèi)容是有問題的(PS:這的確比較尷尬),再次對結(jié)果進行驗證之后重新修改了之前的結(jié)論。幸虧文章閱讀量不是很多,希望被誤導的同學能夠在其他地方得到正確結(jié)論。
上一篇文章 自定義ClassLoader和雙親委派機制 講述了 JVM 中的類的加載機制,Android 也是類 JVM 虛擬機那么它的類加載機制是什么呢,我們來探究一下(PS:文章源碼為 Android5.1 )。
前言
Android 的 Dalvik 虛擬機和 Java 虛擬機的運行原理相同都是將對應的 java 類加載在內(nèi)存中運行。而 Java 虛擬機是加載 class 文件,也可以將一段二進制流通過 defineClass 方法生產(chǎn) Class 進行加載(PS: 自定義ClassLoader和雙親委派機制 文章后面的自定義類加載器就是通過這種方式實現(xiàn)的)。Dalvik 虛擬機加載的 dex 文件。dex 文件是 Android 對與 Class 文件做的優(yōu)化,以便于提高手機的性能??梢韵胂?dex 為 class 文件的一個壓縮文件。dex 在 Android 中的加載和 class 在 jvm 中的相同都是基于雙親委派模型,都是調(diào)用ClassLoader 的 loadClass 方法加載類。
Android系統(tǒng)中類加載的雙親委派機制
-
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;
}
//當所有父類節(jié)點的類加載器都沒有找到該類時,當前加載器調(diào)用findClass方法加載。
if (clazz == null) {
try {
clazz = findClass(className);
} catch (ClassNotFoundException e) {
e.addSuppressed(suppressed);
throw e;
}
}
}
- 想要動態(tài)加載類,可以用 自定義ClassLoader和雙親委派機制 中自定義
ClassLoader的方法加載自己定義的class文件么?看看Android源碼中的ClassLoader的findClass方法:
protected Class<?> findClass(String className) throws ClassNotFoundException {
throw new ClassNotFoundException(className);
}
這個方法直接拋出了 ClassNotFoundException 異常,所以在 Android 中想通過這種方式實現(xiàn)類的加載時不行的。
Android系統(tǒng)中的類加載器
-
Android系統(tǒng)屏蔽了ClassLoader的findClass加載方法,那么它自己的類加載時通過什么樣的方式實現(xiàn)的呢? -
Android系統(tǒng)中有兩個類加載器分別為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) {
//循環(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 中的 ClassLoader 的findClass 方法的實現(xiàn)被取消了,但是 ClassLoader 的基類 BaseDexClassLoader 實現(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 動態(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文件地址,多個地址可以用":"進行分隔optimizedDirectory:制定輸出dex優(yōu)化后的odex文件,可以為nulllibraryPath:動態(tài)庫路徑(將被添加到app動態(tài)庫搜索路徑列表中)parent:制定父類加載器,以保證雙親委派機制從而實現(xiàn)每個類只加載一次。
可以看出 PathClassLoader 和 DexClassLoader 的區(qū)別就在于構(gòu)造函數(shù)中 optimizedDirectory 這個參數(shù)。PathClassLoader 中 optimizedDirectory 為 null,DexClassLoader 中為 new File(optimizedDirectory)。
optimizedDirectory 的作用
BaseDexClassLoader 的構(gòu)造函數(shù)利用 optimizedDirectory 創(chuàng)建了一個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 不同生產(chǎn)的 DexFile 對象不同,我們繼續(xù)看看 optimizedDirectory 在 DexFile 中的作用:
public DexFile(File file) throws IOException {
this(file.getPath());
}
/**
* 從給定的File對象打開一個DEX文件。這通常是一個ZIP/JAR 文件,其中包含“classes.dex”。
* VM將在/data/dalvik-cache中生成相應文件的名稱,然后打開它,如果允許系統(tǒng)權(quán)限,可能會首先創(chuàng)建或更新它。不要在/data/dalvik-cache中傳遞文件名,因為預期該命名文件為原始狀態(tài)(pre-dexopt)。
* @param fileName 引用實際DEX文件的File對象
* @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);
}
/**
* 打開一個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);
//打開一個DEX文件。返回的值是VM cookie。 失敗時,將引發(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);
}
從注釋當中就可以看到 new DexFile(file) 的dex輸出路徑只能為 /data/dalvik-cache,而 DexFile.loadDex() 的 dex 輸出路徑為自己輸入的optimizedDirectory 路徑。

解決疑問
我們在文章開始提出的問題就這樣一步步得到了答案。
DexClassLoader:能夠加載jar/apk/dex,也可以指定對dex優(yōu)化后的odex的輸出文件目錄;PathClassLoader:只能夠加載jar/apk/dex,但它的optimizedDirectory沒法自己設定;
所以 Android 系統(tǒng)默認的類加載器為 PathClassLoader,而DexClassLoader 可以像 JVM 的 ClassLoader 一樣提供動態(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());
}
}
重點關注 @param optimizedDirectory this parameter is deprecated and has no effect ,表明在 Android26 上 optimizedDirectory 已經(jīng)被棄用。
所以在 Android26 之后 PathClassLoader 和 DexClassLoader 已經(jīng)沒有了區(qū)別。
從JVM到Dalivk再到ART(class,dex,odex,vdex,ELF) 這篇文章中講述了 Android26 中其他 dex 相關的優(yōu)化。
總結(jié)
-
ClassLoader的loadClass方法保證了雙親委派機。 -
BaseDexClassLoader提供了兩種派生類使我們可以加載自定義類。
另外還有一個問題自己沒太搞清楚,默認的optimizedDirectory 是哪個路徑?
data/app 和 data/dalvik-cache 下面都沒有我加載的外部的 dex 文件,有誰找到了結(jié)果可以分享一下。