關於DexClassLoader和PathClassLoader,以及Dalvik載入類的過程
android中,dalvik虛擬機器載入的是dex檔案,用於載入類的ClassLoader是PathClassLoader和DexClassLoader。PathClassLoader和DexClassLoader都繼承自BaseDexClassLoader,它們的父ClassLoader為BootClassLoader。
回顧在JVM中,自定義的ClassLoader一般直接繼承自ClassLoader類,為了滿足雙親委派模型,重寫findClass類來將類的全限定名轉化為Class。在findClass中,一般會呼叫ClassLoader的defineClass方法來將byte[]形式的JVM位元組碼轉化為對應的Class。
但是在dalvik虛擬機器上,JVM的位元組碼是無法執行在上面執行的,所以不能通過defineClass來生成dalvik所需要的類。dalvik為了解決這個問題,在android上載入類的ClassLoader主要為PathClassLoader和DexClassLoader,這兩種ClassLoader通過讀取dex再呼叫一些native的方法,可以載入程式執行時所需要的類。
在Android Sdk中的PathClassLoader和DexClassLoader無法看到原始碼,所以想要看具體原始碼實現,可以去看android_libcore的原始碼
DexClassLoader和PathClassLoader的異同
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}
}
}
從上面看出,兩種ClassLoader在實現上只是構造方法不同:PathClassLoader在呼叫父類構造方法時optimizedDirectory傳入為null;DexClassLoader卻傳了一個new File(optimizedDirectory)進去。
optimizedDirectory是什麼
/**
* 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 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, optimizedDirectory);
}
註釋中說optimizedDirectory是optimized dex的路徑,也就是傳說中的odex。也就是說,我們可以將自己指定的odex路徑作為引數傳入DexClassLoader,odex中的相關類將被載入。但是PathClassLoader在構造方法中,已經將optimizedDirectory寫死為null了,所以PathClassLoader從原則上是無法載入使用者指定的odex的。
BUT!!!,在BaseDexClassLoader中有一個方法:
/**
* @hide
*/
public void addDexPath(String dexPath) {
pathList.addDexPath(dexPath, null /*optimizedDirectory*/);
}
這個方法從字面意思上可以手動新增一個dexPath路徑,從而在類載入時會查詢這個路徑。只是這個方法是個hide方法,個人認為addDexPath不推薦使用者呼叫是為了安全性考慮的。試想,你吃著火鍋,唱著歌,突然就被麻匪截了。。不對,是dexPath路徑已經解析完成了,程式跑的好好的突然你的dexPath不知道被哪個王八蛋玩意給你加了一個,而且還是個原來已經定義的方法,程式突然就crash了。。。
進入addDexPath檢視一下邏輯:
public void addDexPath(String dexPath, File optimizedDirectory) {
final List<IOException> suppressedExceptionList = new ArrayList<IOException>();
final Element[] newElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptionList, definingContext);
if (newElements != null && newElements.length > 0) {
final Element[] oldElements = dexElements;
dexElements = new Element[oldElements.length + newElements.length];
System.arraycopy(
oldElements, 0, dexElements, 0, oldElements.length);
System.arraycopy(
newElements, 0, dexElements, oldElements.length, newElements.length);
}
if (suppressedExceptionList.size() > 0) {
final IOException[] newSuppressedExceptions = suppressedExceptionList.toArray(
new IOException[suppressedExceptionList.size()]);
if (dexElementsSuppressedExceptions != null) {
final IOException[] oldSuppressedExceptions = dexElementsSuppressedExceptions;
final int suppressedExceptionsLength = oldSuppressedExceptions.length +
newSuppressedExceptions.length;
dexElementsSuppressedExceptions = new IOException[suppressedExceptionsLength];
System.arraycopy(oldSuppressedExceptions, 0, dexElementsSuppressedExceptions,
0, oldSuppressedExceptions.length);
System.arraycopy(newSuppressedExceptions, 0, dexElementsSuppressedExceptions,
oldSuppressedExceptions.length, newSuppressedExceptions.length);
} else {
dexElementsSuppressedExceptions = newSuppressedExceptions;
}
}
}
主要乾的事情有3件:
- 根據dexPath和自定義的optimizedDirectory,通過makeDexElements生成dexFile相關的結點。
- 將新的Element結點們新增到原來的dexElements中。
- 如果在makeDexElements有錯誤生成,就會把這些錯誤異常儲存起來,放在dexElementsSuppressedExceptions中。
值得注意的是,關於makeDexElements在整個DexPathList中存在兩處被呼叫的地方:一處是剛剛說的addDexPath方法,另一處是構造方法。這也說明了,為什麼BaseDexClassLoader中的addDexPath為什麼是hide的。原因是希望dex查詢路徑在ClassLoader建立時已經確定,後期如果新增會有不確定性的風險。
關於BaseDexClassLoader
不論是DexClassLoader還是PathClassLoader,都是繼承自BaseDexClassLoader。在BaseDexClassLoader中,按照自定義ClassLoader的江湖慣例,實現的是findClass方法,並沒有去實現loadClass方法,所以不論是DexClassLoader還是PathClassLoader都是滿足雙親委派模型的。
看下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轉化的是DexPathList的findClass方法,pathList是在BaseDexClassLoader的構造方法中初始化的。
DexPathList的findClass方法:
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;
}
DexPathList的findClass方法會對dexElements中的每一個element,並通過loadClassBinaryName嘗試進行類載入,一旦載入成功,就會立即返回成功的結果。dexElements是通過makeDexElements產生的。而makeDexElements在上文中提到,只在DexPathList的構造方法和addDexPath方法被呼叫到。而addDexPath作為hide方法有可能不被呼叫,所以可以理解dexElements是在構造方法通過makeDexElements生成的。
findClass中有一個引數是suppressed,這個引數是用於收集從ClassLoader初始化到findClass這個過程中出現的全部的異常。由於類載入時,可能會存在各種各樣的異常資訊,但是當異常發生時DexPathList並沒有立即丟擲異常,而是先catch住,並將這個異常存放在dexElementsSuppressedExceptions這個成員變數中。知道findClass時,通過傳入的suppressed引用返回這個過程中的所有異常。
所以到了現在,理解davik載入類的過程主要就差知道三件事情了:
- DexPathList在建構函式初始化時做了什麼
- makeDexElements做了什麼
- DexFile是怎樣通過loadClassBinaryName生成Class的
DexPathList在建構函式初始化時做了什麼
public DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory) {
//...省略...
this.definingContext = definingContext;
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
// save dexPath for BaseDexClassLoader
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions, definingContext);
// Native libraries may exist in both the system and
// application library paths, and we use this search order:
//
// 1. This class loader's library path for application libraries (librarySearchPath):
// 1.1. Native library directories
// 1.2. Path to libraries in apk-files
// 2. The VM's library path from the system property for system libraries
// also known as java.library.path
//
// This order was reversed prior to Gingerbread; see http://b/2933456.
this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
this.systemNativeLibraryDirectories =
splitPaths(System.getProperty("java.library.path"), true);
List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);
this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories,
suppressedExceptions,
definingContext);
//...省略...
}
一方面是makeDexElements生成了dexElements節點資訊,供findClass時載入;另一方法是將傳入引數的librarySearchPath和系統中java.library.path的路徑合併,共同通過makePathElements生成native類路徑的結點資訊,存放在nativeLibraryPathElements中。nativeLibraryPathElements主要是用於ClassLoader的findLibrary方法獲取系統類的載入路徑。
makeDexElements做了什麼
程式碼略長,makeDexElements主要是根據傳入的類路徑檔名和odex檔案,生成Element結點。類路徑檔名有可能在apk這樣的壓縮檔案中,這樣的路徑中會帶有"!/"這樣的分隔符。也有可能是很原始的dex檔案,甚至是一個資料夾。總之makeElements會根據類路徑檔名生成Element結點儲存在elements中。Element中有一個成員變數是dexFile,DexFile會最終生成Class。
DexFile是怎樣通過loadClassBinaryName生成Class
DexFile會通過loadClassBinaryName,最終呼叫到defineClassNative的方法,並返回Class。defineClassNative是一個Native方法,再之後就是dalvik從平臺層面的原生實現了。
private static native Class defineClassNative(String name, ClassLoader loader, Object cookie,
DexFile dexFile)
所以有一個問題出現了,因為在JVM虛擬機器中可以呼叫defineClass來載入所需要的類,如果在dalvik上呼叫defineClass會發生什麼?
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
{
throw new UnsupportedOperationException("can't load this type of class file");
}
直接拋了UnsupportedOperationException異常。所以雖然和JVM同樣都是雙親委派,但是在具體實現方式上有所不同,因為dalvik的位元組碼和JVM完全不同!dalvik是基於暫存器的虛擬機器,在指令集上更像是8086的彙編;而JVM是基於暫存器的,執行指令時會將運算元和操作符放在棧幀中,返回結果也會返回到棧幀的頂部。
只是天無絕人之路,dalvik同樣有自定義動態載入類的需求,所以才有了DexClassLoader,最終通過defineClassNative這個方法我們想要的類。
總結
我們可以看出,DexClassLoader是通過optimizedDirectory這個引數指定odex的位置,並在初始化時初始化DexPathList,並根據載入路徑與自定義的odex的位置生成Element結點陣列儲存在DexPathList中。通過雙親委派模型的規則,當發現某個類沒有被載入且沒有被父ClassLoader載入時,會呼叫到BasePathClassLoader的findClass方法。然後逐一嘗試載入Element結點陣列中每一個結點的DexFile檔案,呼叫loadClassBinaryName並最終呼叫到defineClassNative這個native方法返回所需要的Class。