一、前言
在 外掛化知識梳理(7) - 類的動態載入入門 中,我們通過一個例子來演示瞭如何通過PathClassLoader
和DexClassLoader
實現類的動態載入。今天這篇文章,我們一起來對這個類載入內部的實現原始碼進行一次簡單的走讀。原始碼的地址為 地址 ,友情提示,需要翻牆。
二、原始碼解析
整個載入過程設計到的類,如下圖所示:
2.1 BaseDexClassLoader
從上面的圖中,我們可以看到DexClassLoader
和PathClassLoader
,都是BaseDexClassLoader
的子類,而當我們呼叫以上兩個類的構造方法時,其實都是呼叫了super()
方法,也就是BaseDexClassLoader
的構造方法,它支援傳入一下四個引數:
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());
}
}
複製程式碼
dexPath
:包含了類和資源的jar/apk
檔案,也就是上一篇例子當中的plugin_dex.jar
,如果有多個檔案,那麼用File.pathSeparator
進行分割,如果我們傳入的是jar/apk
檔案,那麼它會先將裡面的.dex
檔案解壓到記憶體當中,而如果是.dex
檔案,那麼將不會有這一過程。optimizedDirectory
:這個引數目前已經被廢棄了,沒有什麼作用。librarySearchPath
:Native
庫的路徑,同樣可以用File.pathSeparator
進行分割。parent
:父載入器的例項。
而DexClassLoader
和PathClassLoader
的區別就在於後者不支援傳入optimizedDirectory
這個引數,現在看來,對於最新的原始碼,這個引數已經被廢棄了,那麼這兩個類其實是一樣的。但是具體的實現,還是要看手機的安卓版本。
2.2 DexPathList
在前面的例子當中,獲得PathClassLoader/DexClassLoader
例項之後,呼叫了loadClass
方法,它其實呼叫的是基類ClassLoader
中的方法:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
//先檢視該類是否已經被載入過
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//優先呼叫父載入器進行載入.
c = parent.loadClass(name, false);
} else {
//2.如果沒有父載入器,那麼使用 bootstrap 進行載入。
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
//呼叫 findClass 方法。
long t1 = System.nanoTime();
c = findClass(name);
}
}
return c;
}
複製程式碼
對於BaseDexClassLoader
,最終會走到他們重寫的findClass
方法,而該方法又會去通過pathList
去尋找,如果找不到,那麼就會丟擲異常,
@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;
}
複製程式碼
並且其它的公有方法,都是通過pathList
去尋找的,因此這個pathList
是如何構成的就是我們分析原始碼的關鍵。
public DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory) {
this.definingContext = definingContext;
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
//dex/resource (class path) elements
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
//application native library directories
this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
//system native library directories
this.systemNativeLibraryDirectories = splitPaths(System.getProperty("java.library.path"), true);
List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);
//native library path elements
this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories);
if (suppressedExceptions.size() > 0) {
this.dexElementsSuppressedExceptions = suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
} else {
dexElementsSuppressedExceptions = null;
}
}
複製程式碼
在上面的構造方法中,最關鍵的就是通過makeDexElements
和makePathElements
來構建dexElements
和nativeLibraryPathElements
,它們兩個分別為Element
和NativeLibraryElement
型別的陣列,在 外掛化知識梳理(7) - 類的動態載入入門 中,這兩個變數的值為:
2.3 makeDexElements
下面,我們來看一下makeDexElements
的內部實現邏輯:
private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
List<IOException> suppressedExceptions, ClassLoader loader) {
Element[] elements = new Element[files.size()];
int elementsPos = 0;
/*
* Open all files and load the (direct or contained) dex files up front.
*/
for (File file : files) {
if (file.isDirectory()) {
// We support directories for looking up resources. Looking up resources in
// directories is useful for running libcore tests.
elements[elementsPos++] = new Element(file);
} else if (file.isFile()) {
String name = file.getName();
if (name.endsWith(DEX_SUFFIX)) {
// Raw dex file (not inside a zip/jar).
try {
DexFile dex = loadDexFile(file, optimizedDirectory, loader, elements);
if (dex != null) {
elements[elementsPos++] = new Element(dex, null);
}
} catch (IOException suppressed) {
System.logE("Unable to load dex file: " + file, suppressed);
suppressedExceptions.add(suppressed);
}
} else {
DexFile dex = null;
try {
dex = loadDexFile(file, optimizedDirectory, loader, elements);
} 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);
}
if (dex == null) {
elements[elementsPos++] = new Element(file);
} else {
elements[elementsPos++] = new Element(dex, file);
}
}
} else {
System.logW("ClassLoader referenced unknown path: " + file);
}
}
if (elementsPos != elements.length) {
elements = Arrays.copyOf(elements, elementsPos);
}
return elements;
}
複製程式碼
這裡面,最終會建立一個和files
相等大小的elements
陣列,其最終目的是為每個Element
中的dexFile
賦值,而dexFile
則是通過loadDexFile
方法建立的。
private static DexFile loadDexFile(File file, File optimizedDirectory, ClassLoader loader, Element[] elements) throws IOException {
if (optimizedDirectory == null) {
return new DexFile(file, loader, elements);
} else {
String optimizedPath = optimizedPathFor(file, optimizedDirectory);
return DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements);
}
}
複製程式碼
這裡面,會根據optimizedDirectory
的區別,來呼叫DexFile
不同的函式,我們先看靜態方法,可以看到,它裡面也是呼叫了new DexFile
來返回一個DexFile
物件:
static DexFile loadDex(String sourcePathName, String outputPathName,
int flags, ClassLoader loader, DexPathList.Element[] elements) 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, loader, elements);
}
複製程式碼
這裡面,又會呼叫openDex
方法,得到一個mCookie
變數,在前面的例子中,這個mCookie
是一個long
型的物件,對於裡面的內部實現,可以參見這篇文章 跟蹤原始碼分析 Android DexClassLoader 載入機制
pathList
的過程,下面,我們來看一下前面所說的DexFileList
中findClass
的過程。
2.4 尋找 Class 的過程
DexFileList
中尋找類的程式碼如下:
public Class<?> findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
Class<?> clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
複製程式碼
它會遍歷先前構建的Element
陣列,呼叫每個的findClass
方法,直到找到為止,而Element
中的該方法,則會呼叫在2.3
中建立的DexFile
的loadClassBinaryName
來查詢該Class
物件:
public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
return defineClass(name, loader, mCookie, this, suppressed);
}
private static Class defineClass(String name, ClassLoader loader, Object cookie,
DexFile dexFile, List<Throwable> suppressed) {
Class result = null;
try {
result = defineClassNative(name, loader, cookie, dexFile);
} catch (NoClassDefFoundError e) {
if (suppressed != null) {
suppressed.add(e);
}
} catch (ClassNotFoundException e) {
if (suppressed != null) {
suppressed.add(e);
}
}
return result;
}
複製程式碼
三、小結
經過一次簡單的原始碼走讀,我們可以知道,DexClassLoader/PathClassLoader
的內部,為每一個傳入的jar/apk/dex
檔案,都建立了一個Element
變數,它們被儲存在DexFileList
當中,而每一個Element
變數中,又包含了一個關鍵的DexFile
類,之後我們通過DexClassLoader/PathClassLoader
尋找類或者資源時,其實最終都是呼叫了DexFile
中的Native
方法,如果有興趣的同學可以去研究這些方法的內部實現。
最後,簡單地提一下,在Small
的原始碼當中,並沒有直接使用DexClassLoader/PathClassLoader
,它首先是直接呼叫了DexFile
的靜態方法來為每一個外掛建立一個DexFile
:
DexFile
加入到宿主的ClassLoader
當中,而不是像我們之前那樣,為每一個外掛都建立一個ClassLoader
。
該方法中,會像DexFileList
中所做的那樣,通過makeDexElement
方法,為每一個DexFile
建立一個Element
物件:
最後,再將這個物件加入到pathList
變數中:
更多文章,歡迎訪問我的 Android 知識梳理系列:
- Android 知識梳理目錄:www.jianshu.com/p/fd82d1899…
- 個人主頁:lizejun.cn
- 個人知識總結目錄:lizejun.cn/categories/