1、ClassLoader簡介
在應用程序打包成APK時,程序中所創建的類、導入和引用的類都會被編譯打包成一個或多個的dex文件中,打包成的dex文件在使用中如何加載類?答案就在本篇主題ClassLoader中,ClassLoader從字面就可以知道它主要用於類的加載,當代碼中需要調用某個類時,ClassLoader就會遍歷所有的dex文件中的類並保存在集合中,然後從集合中加載指定的Class文件,最終轉換成JVM中使用的類;
2、ClassLoader工作機制
- Android中ClassLoader分類
- PathClassLoader:負責加載系統和apk中的類,context.getClassLoader獲取到的就是PathClassLoader實例;
- DexClassLoader:負責加載外部類(jar或apk),也用於熱修復方案的執行;如:修復的dex文件;
- BaseDexClassLoader:PathClassLoader 和 DexClassLoader的父類,主要的執行邏輯和文件的處理都在其中(後面會分析它的源碼);
- BootClassLoader:繼承與ClassLoader類,一般來說向上傳遞時最頂層就是BootClassLoader,它在Zygote進程啓動開始時,在ZygoteInit.main()方法中執行資源預加載,此時會單例創建BootClassLoader對象,它在loadClass中直接調用findClass(),而findClass()中調用Class.classForName(name, false, null)查找類;
- 獲取ClassLoader的繼承關係
var loader = classLoader
while (loader != null) {
System.out.println(loader)
loader = loader.parent
}
- 獲取輸出的結果:PathClassLoader中parent爲BootClassLoader,在PathClassLoader內部保存這base.apk和library的文件
2019-08-29 13:13:10.444 29022-29022/com.alex.kotlin.optimization I/System.out: dalvik.system.PathClassLoader
[DexPathList[
[zip file "/data/app/com.alex.kotlin.optimization-lLeC3751i-Krivn3eNgrYA==/base.apk"],
nativeLibraryDirectories=[/data/app/com.alex.kotlin.optimization-lLeC3751i-Krivn3eNgrYA==/lib/arm64, /system/lib64, /system/vendor/lib64]]]
2019-08-29 13:13:10.444 29022-29022/com.alex.kotlin.optimization I/System.out: java.lang.BootClassLoader@14d954f
- 源碼繼承關係
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
}
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
}
-
ClassLoader 傳遞性
虛擬機的加載策略:在觸發虛擬機對類的加載時,虛擬機默認使用調用對象中的ClassLoader加載,而調用對象又被調用它的對象中的ClassLoader加載,按此傳遞最終執行到最上層的ClassLoader -
雙親委派機制
- 在每個ClassLoader中都持有其父類的引用,在加載類文件時,首先會判斷當前ClassLoader中是否已經加載過此類,如果加載過從緩存中獲取,否則調用其父類的加載器去查找,父類也會先檢查自己的緩存然後再調用父類查找,直到調用到BootstrapClassLoader(它內部持有的父類爲空),當BootstrapClassLoader沒有找到會向下逐級觸發子類ClassLoader的查找,直到發起者
- 雙親委託機制的好處:(1)先執行父類的查找,避免了同一個類的多次加載;(2)這種先從父類查找的機制,使程序無法修改和替代Java基礎包中的類,提高程序的安全性
3、ClassLoader源碼分析
- loadClass():ClassLoader中加載類時調用loadClass(),代碼中將雙親機制體現的淋漓盡致
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
Class<?> c = findLoadedClass(name); //查找是否加載過此類
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false); //調用父類ClassLoader加載
} else {
c = findBootstrapClassOrNull(name); //父類爲null,表示爲BootstrapClassLoader
}
} catch (ClassNotFoundException e) {
}
if (c == null) { //父類查找爲null,調用自己的查找
c = findClass(name);
}
}
return c;
}
工作流程:
- 在loadClass中首先根據類名判斷虛擬機是否加載過此類,如果加載過則從中獲取
- 對於首次加載的類,先調用ClassLoaser的父類進行加載,如果爲發現繼續向上查詢直到頂層的ClassLoader,此時parent爲null,則調用findBootstrapClassOrNull(),而在findBootstrapClassOrNull()中直接返回null
- 如果都爲發現則從頂層向下逐層的子ClassLoader調用findClass()查找並加載類
- findClass():根據文件名稱查找類文件,此方法主要給字類重寫,實現具體的查找功能,稍後在BaseDexClassLoader中即可看出其中的用法;
//在ClassLoader直接拋出異常
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
4、BaseDexClassLoader源碼分析
由上面的分析知道PathClassLoader和DexClassLoader都是BaseDexClassLoader的自類,PathClassLoader用於加載系統和app中的文件,在Zygote進程中啓動系統服務時創建,DexClassLoader負責加載指定目錄中的文件,從二者的代碼中也可以看出都是直接使用BaseDexClassLoader中的方法,所以程序中類的加載基本都是BaseDexClassLoader在工作,以下一起看看BaseDexClassLoader的工作原理
- 構造函數
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);//創建DexPathList實例
}
從代碼中看出BaseDexClassLoader的構造函數中創建DexPathList的實例,將然如的參數封裝在DexPathList當中,其實這裏不只是創建實例而是執行了整個過程,先看看傳入這幾個參數的意義:
- this:ClassLoader對象,在DexPathList保存並使用
- dexPath:dex文件的路徑,主要加載dex、apk等
- librarySearchPath:本地文件路徑,主要加載程序引入的so庫
- optimizedDirectory:加載dex文件優化後的文件存儲路徑,dex處理過的文件保存在此文件夾下
- findClass():調用pathList中的方法查找Class
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions); // 委託pathList查找類文件
return c;
}
在findClass()方法中調用pathList的方法,所以整個邏輯的執行好像都在pathList中,這裏先來看看DexPathList中的基本屬性,具體看代碼中註釋:
private static final String DEX_SUFFIX = ".dex”; //dex文件後綴
private static final String zipSeparator = "!/“; //zip文件後綴
private final ClassLoader definingContext; //執行加載的ClassLoader,在創建時賦值
private Element[] dexElements; //查找之後保存的Element數組
private final Element[] nativeLibraryPathElements; // 本地的Elements列表
private final List<File> nativeLibraryDirectories;
private final List<File> systemNativeLibraryDirectories;
private IOException[] dexElementsSuppressedExceptions;
在DexPathList的構造函數中直接調用了makeDexElements(),整個加載解析的過程都在這個方法中,會遍歷文件集合中的文件,找出jar、apk、dex文件並保存在Element中,最後返回Element數組;
- makeDexElements()
private static Element[] makeElements(List<File> files, File optimizedDirectory,
285 List<IOException> suppressedExceptions,
286 boolean ignoreDexFiles,
287 ClassLoader loader) {
288 Element[] elements = new Element[files.size()]; //創建Element集合
289 int elementsPos = 0;
294 for (File file : files) { //循環便利所有的File
295 File zip = null; //創建Zip、dir、DexFile實例
296 File dir = new File("");
297 DexFile dex = null;
298 String path = file.getPath(); //獲取文件的路徑和名稱
299 String name = file.getName();
300
301 if (path.contains(zipSeparator)) { //處理zip文件後綴
302 String split[] = path.split(zipSeparator, 2);
303 zip = new File(split[0]);
304 dir = new File(split[1]);
305 } else if (file.isDirectory()) {
308 elements[elementsPos++] = new Element(file, true, null, null); //保存目錄文件到Elements集合中
309 } else if (file.isFile()) {
310 if (!ignoreDexFiles && name.endsWith(DEX_SUFFIX)) { //處理.dex文件後綴
312 try {
313 dex = loadDexFile(file, optimizedDirectory, loader, elements); //創建DexFile實例
314 } catch (IOException suppressed) {
316 suppressedExceptions.add(suppressed);
317 }
318 } else {
319 zip = file;
321 if (!ignoreDexFiles) {
322 try {
323 dex = loadDexFile(file, optimizedDirectory, loader, elements);
324 } catch (IOException suppressed) {
332 suppressedExceptions.add(suppressed);
333 }
334 }
335 }
336 }
339
340 if ((zip != null) || (dex != null)) { //保存Element
341 elements[elementsPos++] = new Element(dir, false, zip, dex);
342 }
343 }
344 if (elementsPos != elements.length) {
345 elements = Arrays.copyOf(elements, elementsPos);
346 }
347 return elements;
348 }
具體執行細節見代碼中註釋,這裏簡單總結一下:makeElements()方法中遍歷傳入的文件集合,查找集合中所有的apk、jar、dex文件及文件夾下所有的dex文件,對於文件目錄直接創建Element封裝文件,對於dex、apk、jar文件則創建DexFile封裝文件、ClassLoader、本地目錄、Elements信息,然後將DexFile和文件封裝成Element保存,因爲最後的文件加載是獲取Elements的集合中保存Element實例,然後調用實例中的DexFile進行加載;
- DexPathList中Class加載過程
public Class findClass(String name, List<Throwable> suppressed) {
414 for (Element element : dexElements) { //遍歷DexFileelements數組
415 DexFile dex = element.dexFile; //獲取Element中的DexFile
417 if (dex != null) { //調用DexFile方法加載Class類,對於目錄來說此時dex = null
418 Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
419 if (clazz != null) {
420 return clazz;
421 }
422 }
423 }
424 if (dexElementsSuppressedExceptions != null) {
425 suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
426 }
427 return null;
428 }
由前面學習知道Class的加載是在DexPathList.findClass()中執行的,在findClass()中主要助興:
- 根據前面獲取到的所有Elements的集合,遍歷取出集合中的Element,從取出的Element中獲的DexFile實例,然後調用DexFile.loadClassBinaryName()加載類
- 在DexFile.loadClassBinaryName()中調用native方法defineClassNative(name, loader, cookie, dexFile);進行類的加載,程序由此進入Native層,這裏不做討論
private static native Class defineClassNative(String name, ClassLoader loader, Object cookie,DexFile dexFile)
5、利用ClassLoader加載補丁文件
由ClassLoader加載機制知道,再查找類時不管子類或是父類只返回最先查到的一個,即DexPathList中保存Elemets集合中靠前的一個,而且系統提供了DexClassLoader讓我們自己加載dex文件,那麼從這原理我們可以發現似乎我們有操作和替換系統類的機會,也的確如此,熱修復的原理就是替換ClassLoader中解析出的Elements中順序,讓修復後的類被優先加載,從而拋棄有Bug的類,如QQ的超級補丁
將這裏根據上面的知識編寫dex文件加載工具類,在編寫代碼之前先分析一下代碼執行的邏輯:
- 創建ClassLoader實例,並使用其加載指定路徑的dex文件獲取Elements數組
- 獲取系統自身的ClassLoader中的Elements數組
- 組合前面獲取的數組
- 反射設置pathList的新數組
- 加載路徑下的dex文件
//創建ClassLoader傳入dex文件路徑即可完成加載
DexClassLoader dexClassLoader = new DexClassLoader(path, optDir, path, getPathClassLoader());
- 反射獲取上面加載獲得的Elements數組
Object elementsNew = getElements(getPathList(dexClassLoader));
//從dexClassLoader實例中創建的pathList
private static Object getPathList(BaseDexClassLoader classLoader) {
Object o = null;
try {
Field pathList = classLoader.getClass().getField("pathList"); //反射獲取pathList
o = pathList.get(classLoader);
} catch (Exception e) {
e.printStackTrace();
}
return o;
}
// 從PathList中獲取加載的Elements數組
private static Object getElements(Object pathList) {
Object o = null;
try {
Field pathListField = pathList.getClass().getField("dexElements");//反射
o = pathListField.get(pathList);
} catch (Exception e) {
e.printStackTrace();
}
return o;
}
- 使用同樣的方式獲取系統PathClassLoader中的Elements
Object elements = getElements(getPathList(getPathClassLoader()));
private static PathClassLoader getPathClassLoader() {
return (PathClassLoader) DexUtils.class.getClassLoader();
}
- 組合Elements數組,Newa中採取的方式是將加載的Path中的Elements數組放在系統加載的數組前面,即可在查找時獲得新的數組中的類,實現熱修復
private static Object combineArray(Object firstArray, Object secondArray) {
Class<?> localClass = firstArray.getClass().getComponentType();
int firstArrayLength = Array.getLength(firstArray);
int allLength = firstArrayLength + Array.getLength(secondArray);
Object result = Array.newInstance(localClass, allLength);
for (int k = 0; k < allLength; ++k) {
if (k < firstArrayLength) {
Array.set(result, k, Array.get(firstArray, k));
} else {
Array.set(result, k, Array.get(secondArray, k - firstArrayLength));
}
}
return result;
}
- 反射設置組合後的Elements數組
private static void setElements(Object pathList, Object o) {
try {
Field pathListField = pathList.getClass().getField("dexElements");
pathListField.set(pathList, o);
} catch (Exception e) {
e.printStackTrace();
}
}
到此關於ClassLoader的介紹就結束了,認識ClassLoader加載機制對熱修復的學習有很大的幫助,本篇也作爲熱修復學習的第一篇,之後會繼續更新熱修復的學習;