Android熱修復之路——ClassLoader詳解

1、ClassLoader簡介

在應用程序打包成APK時,程序中所創建的類、導入和引用的類都會被編譯打包成一個或多個的dex文件中,打包成的dex文件在使用中如何加載類?答案就在本篇主題ClassLoader中,ClassLoader從字面就可以知道它主要用於類的加載,當代碼中需要調用某個類時,ClassLoader就會遍歷所有的dex文件中的類並保存在集合中,然後從集合中加載指定的Class文件,最終轉換成JVM中使用的類;

2、ClassLoader工作機制

  • Android中ClassLoader分類
  1. PathClassLoader:負責加載系統和apk中的類,context.getClassLoader獲取到的就是PathClassLoader實例;
  2. DexClassLoader:負責加載外部類(jar或apk),也用於熱修復方案的執行;如:修復的dex文件;
  3. BaseDexClassLoader:PathClassLoader 和 DexClassLoader的父類,主要的執行邏輯和文件的處理都在其中(後面會分析它的源碼);
  4. 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
   }
  1. 獲取輸出的結果: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
  1. 源碼繼承關係
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

  • 雙親委派機制

  1. 在每個ClassLoader中都持有其父類的引用,在加載類文件時,首先會判斷當前ClassLoader中是否已經加載過此類,如果加載過從緩存中獲取,否則調用其父類的加載器去查找,父類也會先檢查自己的緩存然後再調用父類查找,直到調用到BootstrapClassLoader(它內部持有的父類爲空),當BootstrapClassLoader沒有找到會向下逐級觸發子類ClassLoader的查找,直到發起者
  2. 雙親委託機制的好處:(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;
}

工作流程:

  1. 在loadClass中首先根據類名判斷虛擬機是否加載過此類,如果加載過則從中獲取
  2. 對於首次加載的類,先調用ClassLoaser的父類進行加載,如果爲發現繼續向上查詢直到頂層的ClassLoader,此時parent爲null,則調用findBootstrapClassOrNull(),而在findBootstrapClassOrNull()中直接返回null
  3. 如果都爲發現則從頂層向下逐層的子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當中,其實這裏不只是創建實例而是執行了整個過程,先看看傳入這幾個參數的意義:

  1. this:ClassLoader對象,在DexPathList保存並使用
  2. dexPath:dex文件的路徑,主要加載dex、apk等
  3. librarySearchPath:本地文件路徑,主要加載程序引入的so庫
  4. 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()中主要助興:

  1. 根據前面獲取到的所有Elements的集合,遍歷取出集合中的Element,從取出的Element中獲的DexFile實例,然後調用DexFile.loadClassBinaryName()加載類
  2. 在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文件加載工具類,在編寫代碼之前先分析一下代碼執行的邏輯:

  1. 創建ClassLoader實例,並使用其加載指定路徑的dex文件獲取Elements數組
  2. 獲取系統自身的ClassLoader中的Elements數組
  3. 組合前面獲取的數組
  4. 反射設置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加載機制對熱修復的學習有很大的幫助,本篇也作爲熱修復學習的第一篇,之後會繼續更新熱修復的學習;

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章