Android動態(tài)加載之ClassLoader加載和插件熱修復(fù)的機制原理詳解
前言
深入理解Android中的類加載器
ClassLoader類加載,是動態(tài)加載機制及現(xiàn)在火熱的插件化機制中很基礎(chǔ)但同時又很重要的知識點;
今天我們就來講解下
一、ClassLoader介紹
1、Android中的ClassLoader
- Java中的 ClassLoader可以加載 jar 文件和Class文件(本質(zhì)時加載Class文件)。在Android中,它們加載到是dex文件;
- Android中的ClassLoader類型分別是系統(tǒng)類加載器和自定義加載器。其中系統(tǒng)類加載器主要包括3種,分別是 BootClassLoader 、PathClassLoader 和 DexClassLoader;
- BootClassLoader: Dalvik/ART虛擬機用于加載Android系統(tǒng)類的Loader,應(yīng)用層通過獲取父ClassLoader的最終項;
- PathClassLoader: 我們知道,打包APK后實際上是把java文件都生成dex文件,而這個Loader就是在應(yīng)用啟動時,加載已安裝APK的dex文件;
- DexClassLoader: 常見的動態(tài)加載機制都用這個類,傳入指定路徑加載指定dex文件;
- PathClassLoader和DexClasLoader都是繼承自 dalviksystem.BaseDexClassLoader,它們的類加載邏輯全部寫在BaseDexClassLoader中;
2、加載原理
ClassLoader使用的是雙親委托機制。雙親委派模型,旨在于讓頂級父類加載器先加載類,若不成功,則一層層往下加載,最終到當(dāng)前加載器。這樣做的目的是保持類加載系統(tǒng)的穩(wěn)定性,不會出現(xiàn)不同加載器加載同一個類時,出現(xiàn)多個類實例;
- 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;
- }
- if (clazz == null) {
- try {
- clazz = findClass(className);
- } catch (ClassNotFoundException e) {
- e.addSuppressed(suppressed);
- throw e;
- }
- }
- }
- return clazz;
- }
- 會先查詢當(dāng)前ClassLoader實例是否加載過此類,有就返回;
- 如果沒有。查詢Parent是否已經(jīng)加載過此類,如果已經(jīng)加載過,就直接返回Parent加載的類;
- 如果繼承路線上的ClassLoader都沒有加載,才由Child執(zhí)行類的加載工作;
- 這樣做有個明顯的特點,如果一個類被位于樹根的ClassLoader加載過,那么在以后整個系統(tǒng)的生命周期內(nèi),
- 這個類永遠不會被重新加載;
- 如果希望通過動態(tài)加載的方式,加載一個新版本的dex文件,使用里面的新類替換原有的舊類,從而修復(fù)原有類的BUG,那么必須保證在加載新類的時候,舊類還沒有被加載,因為如果已經(jīng)加載過舊類,那么ClassLoader會一直優(yōu)先使用舊類;
二、ClassLoader源碼分析
1、PathClassLoader
Android主要關(guān)心的是PathClassLoader和DexClassLoader;
PathClassLoader用來操作本地文件系統(tǒng)中的文件和目錄的集合。并不會加載來源于網(wǎng)絡(luò)中的類。Android采用這個類加載器一般是用于加載系統(tǒng)類和它自己的應(yīng)用類。這個應(yīng)用類放置在data/data/包名下;
看一下PathClassLoader的源碼,只有2個構(gòu)造方法:
- package dalvik.system;
- 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);
- }
- }
2、DexClassLoader
- DexClassLoader可以加載一個未安裝的APK,也可以加載其它包含dex文件的JAR/ZIP類型的文件。DexClassLoader需要一個對應(yīng)用私有且可讀寫的文件夾來緩存優(yōu)化后的class文件;
- 而且一定要注意不要把優(yōu)化后的文件存放到外部存儲上,避免使自己的應(yīng)用遭受代碼注入攻擊;
- package dalvik.system;
- import java.io.File;
- public class DexClassLoader extends BaseDexClassLoader {
- public DexClassLoader(String dexPath, String optimizedDirectory,
- String libraryPath, ClassLoader parent) {
- super(dexPath, new File(optimizedDirectory), libraryPath, parent);
- }
- }
- PathClassLoader和DexClassLoader除了構(gòu)造方法傳參不同,其它的邏輯都是一樣的;
- 要注意的是DexClassLoader構(gòu)造方法第2個參數(shù)指的是dex優(yōu)化緩存路徑,這個值是不能為空的;
- 而PathClassLoader對應(yīng)的dex優(yōu)化緩存路徑為null是因為Android系統(tǒng)自己決定了緩存路徑;
- Android中具體負責(zé)類加載的并不是哪個ClassLoader,而是通過DexFile的defineClassNative()方法來加載的;
3、BaseDexClassLoader
接下來我們看一下BaseDexClassLoader這個類:
BaseDexClassLoader的構(gòu)造方法有四個參數(shù):
- dexPath,指的是在Androdi包含類和資源的jar/apk類型的文件集合,指的是包含dex文件。多個文件用“:”分隔開,用代碼就是File.pathSeparator;
- optimizedDirectory,指的是odex優(yōu)化文件存放的路徑,可以為null,那么就采用默認的系統(tǒng)路徑;
- libraryPath,指的是native庫文件存放目錄,也是以“:”分隔;
- parent,parent類加載器;可以看到,在BaseDexClassLoader類中初始化了DexPathList這個類的對象。這個類的作用是存放指明包含dex文件、native庫和優(yōu)化目錄;
- # dalvik.system.BaseDexClassLoader
- public BaseDexClassLoader(String dexPath, File optimizedDirectory,
- String libraryPath, ClassLoader parent) {
- super(parent);
- this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
- }
4、DexPathList
- dalvik.system.DexPathList封裝了dex路徑,是一個final類,而且訪問權(quán)限是包權(quán)限,也就是說外界不可繼承,也不可訪問這個類;
- BaseDexClassLoader在其構(gòu)造方法中初始化了DexPathList對象,我們來看一下DexPathList的源碼,我們需要重點關(guān)注一下它的成員變量dexElements,它是一個Element[]數(shù)組,是包含dex的文件集合;
- Element是DexPathList的一個靜態(tài)內(nèi)部類。DexPathList的構(gòu)造方法有4個參數(shù)。從其構(gòu)造方法中也可以看到傳遞過來的classLoade對象和dexPath不能為null,否則就拋出空指針異常;# dalvik.system.DexPathList
- private final Element[] dexElements;
- public DexPathList(ClassLoader definingContext, String dexPath,
- String libraryPath, File optimizedDirectory) {
- if (definingContext == null) {
- throw new NullPointerException("definingContext == null");
- }
- if (dexPath == null) {
- throw new NullPointerException("dexPath == null");
- }
- if (optimizedDirectory != null) {
- if (!optimizedDirectory.exists()) {
- throw new IllegalArgumentException(
- "optimizedDirectory doesn't exist: "
- + optimizedDirectory);
- }
- // 如果文件不是可讀可寫的也會拋出異常
- if (!(optimizedDirectory.canRead()
- && optimizedDirectory.canWrite())) {
- throw new IllegalArgumentException(
- "optimizedDirectory not readable/writable: "
- + optimizedDirectory);
- }
- }
- this.definingContext = definingContext;
- ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
- // 通過makeDexElements方法來獲取Element數(shù)組
- // splitDexPath(dexPath)方法是用來把我們之前按照“:”分隔的路徑轉(zhuǎn)為File集合。
- this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
- suppressedExceptions);
- if (suppressedExceptions.size() > 0) {
- this.dexElementsSuppressedExceptions =
- suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
- } else {
- dexElementsSuppressedExceptions = null;
- }
- this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
- }
5、makeDexElements
makeDexElements方法的作用是獲取一個包含dex文件的元素集合;
# dalvik.system.DexPathList
- private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,
- ArrayList<IOException> suppressedExceptions) {
- ArrayList<Element> elements = new ArrayList<Element>();
- // 遍歷打開所有的文件并且加載直接或者間接包含dex的文件。
- for (File file : files) {
- File zip = null;
- DexFile dex = null;
- String name = file.getName();
- if (file.isDirectory()) {
- // We support directories for looking up resources.
- // This is only useful for running libcore tests.
- // 可以發(fā)現(xiàn)它是支持傳遞目錄的,但是說只測試libCore的時候有用
- elements.add(new Element(file, true, null, null));
- } else if (file.isFile()){
- // 如果文件名后綴是.dex,說明是原始dex文件
- if (name.endsWith(DEX_SUFFIX)) {
- // Raw dex file (not inside a zip/jar).
- try {
- //調(diào)用loadDexFile()方法,加載dex文件,獲得DexFile對象
- dex = loadDexFile(file, optimizedDirectory);
- } catch (IOException ex) {
- System.logE("Unable to load dex file: " + file, ex);
- }
- } else {
- // dex文件包含在其它文件中
- zip = file;
- try {
- // 同樣調(diào)用loadDexFile()方法
- dex = loadDexFile(file, optimizedDirectory);
- } catch (IOException suppressed) {
- // 和加載純dex文件不同的是,會把異常添加到異常集合中
- /*
- * 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);
- }
- }
- } else {
- System.logW("ClassLoader referenced unknown path: " + file);
- }
- // 如果zip或者dex二者一直不為null,就把元素添加進來
- // 注意,現(xiàn)在添加進來的zip存在不為null也不包含dex文件的可能。
- if ((zip != null) || (dex != null)) {
- elements.add(new Element(file, false, zip, dex));
- }
- }
- return elements.toArray(new Element[elements.size()]);
- }
6、loadDexFile()、loadDex
通過上面的代碼也可以看到,加載一個dex文件調(diào)用的是loadDexFile()方法;
# dalvik.system.DexPathList
- private static DexFile loadDexFile(File file, File optimizedDirectory)
- throws IOException {
- // 如果緩存存放目錄為null就直接創(chuàng)建一個DexFile對象返回
- if (optimizedDirectory == null) {
- return new DexFile(file);
- } else {
- // 根據(jù)緩存存放目錄和文件名得到一個優(yōu)化后的緩存文件路徑
- String optimizedPath = optimizedPathFor(file, optimizedDirectory);
- // 調(diào)用DexFile的loadDex()方法來獲取DexFile對象。
- return DexFile.loadDex(file.getPath(), optimizedPath, 0);
- }
- }
DexFile的loadDex()方法如下,內(nèi)部也做了一些調(diào)用。拋開這些細節(jié)來講,它的作用就是加載DexFile文件,而且會把優(yōu)化后的dex文件緩存到對應(yīng)目錄;
# dalvik.system.DexFile
- static public DexFile loadDex(String sourcePathName, String outputPathName,
- int flags)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.
- */
- //loadDex方法內(nèi)部就是調(diào)用了DexFile的一個構(gòu)造方法
- return new DexFile(sourcePathName, outputPathName, 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);
- }
- 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);
- }
- private static native long openDexFileNative(String sourceName, String outputName, int flags);
- 在BaseDexClassLoader對象構(gòu)造方法內(nèi),創(chuàng)建了PathDexList對象。而在PathDexList構(gòu)造方法內(nèi)部,通過調(diào)用一系列方法,把直接包含或者間接包含dex的文件解壓縮并緩存優(yōu)化后的dex文件,通過PathDexList的成員變量 Element[] dexElements來指向這個文件;
- 到此我們就分析完了BaseDexClassLoader的構(gòu)造方法;
7、loadClass
- 之前講Java類加載器的時候已經(jīng)說了,類加載是按需加載,也就是說當(dāng)明確需要使用class文件的時候才會加載;
- 與在Java中的loadClass()方法主要流程是類似的,不過因為Android中BootClassLoader是用Java代碼寫的,所以可以直接當(dāng)作系統(tǒng)類加載器的parent類加載器。在Android中如果parent類加載器找不到類,最終還是會調(diào)用ClassLoader對象自己的findClass()方法;
- 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;
- }
- if (clazz == null) {
- try {
- clazz = findClass(className);
- } catch (ClassNotFoundException e) {
- e.addSuppressed(suppressed);
- throw e;
- }
- }
- }
- return clazz;
- }
我們可以去看一下BaseDexClassLoader類的findClass()方法;
# dalvik.system.BaseDexClassLoader
- @Override
- protected Class<?> findClass(String name) throws ClassNotFoundException {
- List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
- // 調(diào)用DexPathList對象的findClass()方法
- 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;
- }
實際上BaseDexClassLoader調(diào)用的是其成員變量DexPathList pathList的findClass()方法;
# dalvik.system.DexPathList
- public Class findClass(String name, List<Throwable> suppressed) {
- // 遍歷Element
- for (Element element : dexElements) {
- // 獲取DexFile,然后調(diào)用DexFile對象的loadClassBinaryName()方法來加載Class文件。
- 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最終還是遍歷其自身的Element[]數(shù)組,獲取DexFile對象來加載Class文件;
- DexPathList構(gòu)造方法內(nèi)是調(diào)用其makeDexElements()方法來創(chuàng)建Element[]數(shù)組的,而且也提到了如果zip文件或者dex文件二者之一不為null,就把元素添加進來,而添加進來的zip存在不為null也不包含dex文件的可能;
- 上面的代碼中也可以看到,獲取Class的時候跟這個zip文件沒什么關(guān)系,調(diào)用的是dex文件對應(yīng)的DexFile的方法來獲取Class;
- 數(shù)組的遍歷是有序的,假設(shè)有兩個dex文件存放了二進制名稱相同的Class,類加載器肯定就會加載在放在數(shù)組前面的dex文件中的Class;
- 現(xiàn)在很多熱修復(fù)技術(shù)就是把修復(fù)的dex文件放在DexPathList中Element[]數(shù)組的前面,這樣就實現(xiàn)了修復(fù)后的Class搶先加載了,達到了修改bug的目的;
- Android加載一個Class是調(diào)用DexFile的defineClass()方法。而不是調(diào)用ClassLoader的defineClass()方法。這一點與Java不同,畢竟Android虛擬機加載的dex文件,而不是class文件;
# dalvik.system.DexFile
- public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
- return defineClass(name, loader, mCookie, suppressed);
- }
- private static Class defineClass(String name, ClassLoader loader, long cookie,
- List<Throwable> suppressed) {
- Class result = null;
- try {
- result = defineClassNative(name, loader, cookie);
- } catch (NoClassDefFoundError e) {
- if (suppressed != null) {
- suppressed.add(e);
- }
- } catch (ClassNotFoundException e) {
- if (suppressed != null) {
- suppressed.add(e);
- }
- }
- return result;
- }
# java.lang.ClassLoader
- protected final Class<?> defineClass(String className, byte[] classRep, int offset, int length,
- ProtectionDomain protectionDomain) throws java.lang.ClassFormatError {
- throw new UnsupportedOperationException("can't load this type of class file");
- }
Android中加載一個類是遍歷PathDexList的Element[]數(shù)組,這個Element包含了DexFile,調(diào)用DexFile的方法來獲取Class文件,如果獲取到了Class,就跳出循環(huán)。否則就在下一個Element中尋找Class;
三、熱修復(fù)的原理
利用pathClassLoader 的 對dex 文件進行替換,補丁 dex 文件加載到Element對象,并插入到 dexElement前面,具體還是使用反射;
雙親委派:當(dāng)一個class文件被加載時,classloader發(fā)現(xiàn)已經(jīng)加載過則不會重新加載,如果沒加載過則遞歸地把這個請求委派給父類加載器完成。當(dāng)父加載器找不到指定的類時,子加載器嘗試自己加載
步驟
關(guān)鍵是ClassLoader中l(wèi)oadeClass() 方法, loadClass()雙親委托機制
一個dex被加載的步驟
先從自己緩存中取
自己緩存沒有,就在 父 ClassLoader 要 (parent.loadClass())
父 ClassLoader 沒有,就自加載(findClass)
makeDexElements(將dex文件或壓縮包中的信息保存到dexElements中)
findCLass(遍歷Element,并將Element轉(zhuǎn)成Dex文件,獲取Dex文件中的Class文件,直到找到對應(yīng)的class文件位置)
總結(jié)
了解各種加載流程,還是需要多深入源碼,Android-ClassLoader實現(xiàn)邏輯算是非常清晰易懂,但對我們?nèi)粘i_發(fā)如插件化方案會有非常大的幫助;
本文轉(zhuǎn)載自微信公眾號「Android開發(fā)編程」