熱更新之Tinker類加載原理


Android的基礎語言是Java,而Java在運行時虛擬機加載對應的類和資源是通過ClassLoader來實現的,ClassLoader本身是一個抽象來,Android中使用PathClassLoader類作爲Android的默認的類加載器。

Android中的虛擬機不是JVM,而是Dalvik/ART VM。不同於JVM中加載.class,Dalvik/ART加載的是.dex文件。

類加載機制

每個類編譯後產生一個Class對象,存儲在.class文件中,JVM使用類加載器Class Loader來加載類的字節碼文件.class,類加載器實質上是一條類加載器鏈,一般的,我們只會用到一個原生的類加載器,它只加載Java API等可信類,通常只是在本地磁盤中加載,這些類一般就夠我們使用了。

如果我們需要從遠程網絡或數據庫中下載.class字節碼文件,那就需要我們來掛載額外的類加載器。也即類加載器中有多個Class Loader,平時只用一個默認的PathClassLoader,額外情況還需要DexClassPath。

加載策略

父類優先策略是比較一般的情況(如JDK採用的就是這種方式),在這種策略下,類在加載某個Java類之前,會嘗試代理給其父類加載器,只有當父類加載器找不到時,才嘗試自己去加載。
自己優先的策略與父類優先相反,它會首先嚐試自己加載,找不到的時候纔要父類加載器去加載,這種在web容器(如tomcat)中比較常見。

類的加載和初始化

類加載器加載了一個類的.class文件,不意味着該Class對象被初始化。一個類的初始化包括3個步驟:

  1. 加載(Loading),由類加載器執行,查找字節碼,並創建一個Class對象(只是創建);
  2. 鏈接(Linking),驗證字節碼,爲靜態域分配存儲空間(只是分配,並不初始化該存儲空間),解析該類創建所需要的對其它類的應用;
  3. 初始化(Initialization),首先執行靜態初始化塊static{},初始化靜態變量,執行靜態方法(如構造方法)。

注意:類與接口的初始化不同,如果一個類被初始化,則其父類或父接口也會被初始化,但如果一個接口初始化,則不會引起其父接口的初始化。

動態加載

不管使用什麼樣的類加載器,類都是在第一次被用到時,動態加載到JVM的。這個特性就是Java的動態加載特性:

  1. Java程序在運行時並不一定被完整加載,只有當發現該類還沒有加載時,纔去本地或遠程查找類的.class文件並驗證和加載;
  2. 當程序創建了第一個對類的靜態成員的引用(如類的靜態變量、靜態方法、構造方法——構造方法也是靜態的)時,纔會加載該類。

類的鏈接

Java類的鏈接指的是將Java類的二進制代碼合併到JVM的運行狀態之中的過程,它是保障加載的類能在虛擬機中正常運行的必要步驟。鏈接有3個步驟:

  1. 驗證(Verification),驗證是保證二進制字節碼在結構上的正確性,包括檢測類型正確性,
    接入屬性正確性(public、private),檢查final class 沒有被繼承,檢查靜態變量的正確性等。

  2. 準備(Preparation),準備階段主要是創建靜態域,分配空間,給這些域設默認值,
    需要注意的是兩點:一個是在準備階段不會執行任何代碼,僅僅是設置默認值,二個是這些默認值是這樣分配的,
    原生類型全部設爲0,如:float:0f,int 0, long 0L, boolean:0(布爾類型也是0),其它引用類型爲null。

  3. 解析(Resolution),解析的過程就是對類中的接口、類、方法、變量的符號引用進行解析並定位,
    解析成直接引用(符號引用就是編碼使用字符串表示某個變量、接口的位置,直接引用就是根據符號引用翻譯出來的地址),
    並保證這些類被正確的找到。
    解析策略:
    early resolution:要求所有引用都必須存在,所以在解析時遞歸的把所有引用解析
    late resolution:Oracle的JDK所採取的策略,在類只是被引用還未被真正用到時,並不進行解析,只有真正用到時纔會加載和解析這個類。

ClassLoader

類加載器,ClassLoader。
ClassLoader的層級結構:
在這裏插入圖片描述
Android中Dalvik虛擬機中的類加載流程
在這裏插入圖片描述
Android的類加載器分爲兩種PathClassLoaderDexClassLoader,都繼承自BaseDexClassLoader,而BaseDexClassLoader繼承自ClassLoader。
PathClassLoader:用來加載系統類和應用類,已經緩存的dex,如ART的中的所有.dex,是Android虛擬機中的默認的加載器
DexClassLoader:用來加載jar.apk、dex文件,也可從SD卡中進行加載。加載jar、apk最終的實質也是提取了裏面的Dex文件進行加載。

雙親委派模型

DexClassLoader和PathClassLoader都屬於符合雙親委派模型的類加載器(因爲它們沒有重載loadClass方法)。
特點
即當一個加載器被請求加載某個類時,它首先委託自己的父加載器去加載,一直向上查找,若頂級加載器(優先)或父類加載器能加載,則返回這個類所對應的Class對象,若不能加載,則最後再由請求發起者去加載該類。
如果已經加載過了,就會直接將之返回,而不會重複加載。

【注:】這就是爲什麼採用類加載方案的熱修復需要冷啓動生效的原因:補丁合成好之前類已加載,想要替換bug類,需要重新啓動軟件,重新加載修復好的類
表現
類加載的時候會去遍歷dex文件,優先加載前面的dex。類加載熱更新就是應用重啓時加載的就是已經修復問題的dex文件。
優點
這種方式的優點就是能夠保證類的加載按照一定的規則次序進行,越是基礎的類,越是被上層的類加載器進行加載,從而保證程序的安全性。

類加載器關鍵源碼

1. ClassLoader
從源碼中看出,虛擬機中默認的SystemClassLoader是PathClassLoader。

    /**
     * Encapsulates the set of parallel capable loader types.
     * 封裝一組並行的加載器類型。
     */
    private static ClassLoader createSystemClassLoader() {
        String classPath = System.getProperty("java.class.path", ".");
        String librarySearchPath = System.getProperty("java.library.path", "");
        // 看見了吧 PathClassLoader 是默認的類加載器
        return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance());
    }

2. BaseDexClassLoader

    public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }

BaseDexClassLoader除了調用了父類ClassLoader的構造方法,還在構造函數中初始化了一個DexPathList對象,這是一個描述DEX文相關資源文件的條目列表。

3. DexPathList

   final class DexPathList {
  ...
  
    public DexPathList(ClassLoader definingContext, String dexPath, String libraryPath, File optimizedDirectory) {

        .......
        this.definingContext = definingContext;
        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
        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);
    }

    public Class findClass(String name, List<Throwable> suppressed) {
            //遍歷該數組
            for (Element element : dexElements) {
                //初始化DexFile
                DexFile dex = element.dexFile;

                if (dex != null) {
                    //調用DexFile類的loadClassBinaryName方法返回Class實例
                    Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
                    if (clazz != null) {
                        return clazz;
                    }
                }
            }
            return null;
        }
    
...}

從代碼中可以看出,熱修復至關重要的dexElements,就是在BaseDexClassLoader的初始化中進行初始化的。在DexPathList的構造函數中調用makeDexElements解析出dex相關參數,並保存到dexElements成員變量中,dexElements成員的順序決定了.dex的加載順序。

DexPathList的findClass方法就是爲了檢測掃描到的Class,該方法會遍歷dexElements,然後獲取每個Element的DexFile,DexFile不爲空則調用其loadClassBinaryName並返回Class實例。ClassLoader在加載到正確的Class後,對同一Class將不再加載。
findClass方法是ClassLoader的核心

5. makeDexElements
在makeDexElements方法中loadDexFile方法加載dex文件,並返回DexFile對象。

private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,
                                             ArrayList<IOException> suppressedExceptions) {
        ArrayList<Element> elements = new ArrayList<Element>();
        /*
         * Open all files and load the (direct or contained) dex files
         * up front.
         */
        for (File file : files) {
            File zip = null;
            DexFile dex = null;
            String name = file.getName();

            if (name.endsWith(DEX_SUFFIX)) {
                // Raw dex file (not inside a zip/jar).
                try {
                    dex = loadDexFile(file, optimizedDirectory);
                } catch (IOException ex) {
                    System.logE("Unable to load dex file: " + file, ex);
                }
            } else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
                    || name.endsWith(ZIP_SUFFIX)) {
                zip = file;

                try {
                    dex = loadDexFile(file, optimizedDirectory);
                } 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);
                }
            } else if (file.isDirectory()) {
                // We support directories for looking up resources.
                // This is only useful for running libcore tests.
                elements.add(new Element(file, true, null, null));
            } else {
                System.logW("Unknown file type for: " + file);
            }

            if ((zip != null) || (dex != null)) {
                elements.add(new Element(file, false, zip, dex));
            }
        }
        return elements.toArray(new Element[elements.size()]);
    }

6. dexElements

    List of dex/resource (class path) elements.Should be called pathElements,
    but the Facebook app uses reflection to modify 'dexElements'
    dex / resource(類路徑)元素的列表。應稱爲pathElements,但Facebook應用程序使用反射來修改“ dexElements”

dexElements 是一個Element[] ,它是類加載模式熱更新的核心。
它的作用是維護全部的dex文件(我們寫的類的二進制表述方式,用來給安卓虛擬機加載),存在Android程序中。

安卓虛擬機會根據需要從該數組按照自上而下的順序加載對應的類文件,即使數組中存多個同一個類對應的dex文件,虛擬機一旦找到了對應的dex文件就會停止查找,並加載。 根據這個規則,我們只需要把Bug修復涉及到的類文件插入到數組的最前面去,就可以達到修復的目的。

例如:當一個patch.dex放到了dexElements的第一位,那麼當加載一個bug類A時,發現在patch.dex中發現修復過的類A,則直接加載這個類。在加載class.dex時也會掃描到未修復的類A,但是類A已被加載過,將不再重新加載A,即達到了修復的效果。

7. DexClassLoader

   public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath,ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }

dexPath:加載APK、DEX和JAR的路徑。多個路徑之間可以用:分割
這個類可以用於Android動態加載DEX/JAR。
optimizedDirectory: dex文件首次加載時會進行dex opt操作,optimizedDirectory是優化後odex的存放目錄,目錄不爲空,且官方推薦使用應用的私有目錄,dexOutputDir = context.getDir(“dex”, 0)。
libraryPath:加載DEX的時候需要用到的lib動態庫路徑,libraryPath一般包括/vendor/lib和/system/lib。不可爲空
parent:DEXClassLoader指定的父類加載器,ClassLoader參數類型

【注意】

  1. 這個類加載器加載的文件是.jar或者.apk文件,並且這個.jar或.apk中是包含classes.dex這個入口文件的,主要是用來執行那些沒有被安裝的一些可執行文件的。比如熱更新中的Bugly的補丁包是.apk文件,Sophix生成的補丁是.jar文件。

  2. 這個類加載器需要一個屬於應用的私有的目錄作爲它自己的緩存優化目錄。這個目錄也就構造函數的第二個參數(dex輸出路徑)

  3. 不要把上面第二點中提到的這個緩存目錄設爲外部存儲,因爲外部存儲容易受到代碼注入的攻擊。

8. PathClassLoader

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);
	}
} 

android系統採用PathClassLoader作爲其系統加載器以及應用加載器。
PathClassLoader 和DexClassLoader的區別就在於optimizedDirectory參數是否爲空

9. findLoadedClass
這個方法在ClassLoader中,其子類沒有該方法。調用了findLoadedClass查找當前虛擬機是否已經加載過該類,是則直接返回該class。如果未加載過,則調用父加載器的loadClass方法,這裏採用了java的雙親委派模型。

 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;
    }

10.DexClassLoader與PathClassLoader的區別
在構造方法中PathClassLoader的optimizedDirectory參數可以爲空,DexClassLoader中的參數不可爲空。PathClassLoader會自動生成一個緩存目錄/data/dalvik-cache/[email protected]。DexClassLoader是使用系統默認的緩存路徑。

所以一般PathDexClassLoader只能加載已安裝的apk的dex,而DexClassLoader則可以加載指定路徑的apk、dex和jar,也可以從sd卡中進行加載。

在dex分包的時候,我們通過PathClassLoader獲取已加載的保存在pathList中的dex信息,然後利用DexClassLoadder加載我們指定的從dex文件,將dex信息合併到pathList的dexElements中,從而在app運行的時候能夠將所有的dex中的類加載到內存中。

類加載熱更新原理

  1. 通過獲取到當前應用的Classloader,即爲BaseDexClassloader
  2. 通過反射獲取到他的DexPathList屬性對象pathList
  3. 通過反射調用pathList的dexElements方法把patch.dex轉化爲Element[]
  4. 兩個Element[]進行合併,把合併的fix.dex放到dexElements最前面去
  5. 加載Element[],達到修復目的

Tinker中的熱更新流程

在這裏插入圖片描述
在這裏插入圖片描述

  1. Tinker 方案參考了multidex的實現原理,在編譯時通過新舊兩個APK的Dex生成差異patch.dex。
  2. 通過相關平臺將patch.apk下發到終端,patch.dex與舊版.dex合併還原成新的.dex。
  3. 將新合成的dex插入dexElements數組最前面,使得新的.dex中的內容Class優先加載
  4. 下次啓動程序,bug得到修復

其中有些常識需要額外注意

  1. 爲了減小patch的大小,Tinker自研了DexDiff算法,深度利用Dex的格式減小差異大小。讓patch只包含有差異的部分,相同的部分不包含
  2. Tinker的補丁包形式是.apk格式,除了patch.dex,還包含resource差異包等文件
  3. 由於合併的過程比較費時,所以有一個單獨的PatchService進行合併。
  4. 類加載實現原理涉及了dex文件的重新解壓縮合並等處理,消耗的內存大,時間長,系統內存低時容易合併失敗。也即類加載熱更新存在一定的失敗率。

QZone與Tinker的類加載有點區別, QZone不用patch.dex與舊的.dex合併,所以會出現CLASS_ISPREVERIFIED問題。而Tinker的patch.dex與舊的.dex合併後在同一個.dex中,其實是將patch.dex的內容順序排在了修復.dex的前面,能優先加載.patch裏面的類。

爲弄清楚類加載機制的熱更新專門寫的該篇博客,希望能幫助大家理解基於類加載方案的熱更新原理。

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