1. 程式人生 > >Android 熱修復原理,DVM或ART與JVM的介紹ClassLoad及雙親委派模型理解

Android 熱修復原理,DVM或ART與JVM的介紹ClassLoad及雙親委派模型理解

熱修復說白了就是”打補丁”,通過事先設定的介面從網上下載無Bug的程式碼來替換有Bug的程式碼。這樣就省事多了,使用者體驗也好。這樣帶來的優勢就是成本低、效率高。熱修復的特點:無需重新發版,實時高效熱修復;使用者無感知修復,無需下載新的應用,代價小;修復成功率高,把損失降到最低。但是,Android是如何實現熱修復的呢?這一次要從DVM(Dalvik虛擬機器)與 JVM(JAVA虛擬機器)的載入類原理講起。

相關文章連線:

一、JVM機制

JVM(Java Virtual Machine)即Java虛擬機器,它可以通過 類載入器 把 Class檔案 載入到自己 執行時記憶體中去執行。虛擬機器是執行在作業系統中的,而程序又是作業系統的執行單位,所以當java虛擬機器執行的時候,它就是作業系統中的程序例項單位,當它沒執行時,可以把它叫做程式。總而言之,Java程式在執行的時候,JVM通過類載入機制(ClassLoader)把.class檔案載入到記憶體中。只有class檔案被載入記憶體,才能被其他class引用,使程式正確執行起來。

JVM類載入機制(ClassLoader)

  1. Bootstrap ClassLoader :啟動類載入器,負責載入java基礎類,對應的檔案是%JRE_HOME/lib/ 目錄下的rt.jar、resources.jar、charsets.jar和class等;
  2. Extension ClassLoader:擴充套件類載入器,對應的檔案是 %JRE_HOME/lib/ext 目錄下的jar和class等;
  3. App ClassLoader:系統類載入器,對應的檔案是應用程式classpath目錄下的所有jar和class等。

工作原理:雙親委派機制

  三種ClassLoader存在父子關係,App ClassLoader的父類載入器是Extension ClassLoader,Extension ClassLoader的父類載入器是Bootstrap ClassLoader。注意這裡的父子並不是繼承關係。Java的類載入使用雙親委託機制來搜尋類,即當這三者中的某個ClassLoader要載入一個類時,會先委託它的父類載入器嘗試載入,一直往上,如果最頂級的父類載入器沒有找到該類,那麼委託者則親自到特定的地方載入,如果沒找到,那麼就丟擲異常ClassNotFoundException。

案例驗證:

public class Test {  
  
    public static void main(String[] args) {  
        ClassLoader ClassLoader1 = Test.class.getClassLoader();  
        ClassLoader ClassLoader2 = ClassLoader1.getParent();  
        ClassLoader ClassLoader3 = ClassLoader2.getParent();  
  
        System.out.println(ClassLoader1);  
        System.out.println(ClassLoader2);  
        System.out.println(ClassLoader3);  
    }  
}  

輸出結果:

  1. sun.misc.Launcher$AppClassLoader@1bbf1ca  
  2. sun.misc.Launcher$ExtClassLoader@1ff0dde  
  3. null

二、DVM機制 

Dalvik是Google為Android平臺設計的虛擬機器,以.dex(Dalvik Executable)格式作為虛擬機器的壓縮格式,適用於記憶體和處理器有限的作業系統。Delvik允許同時執行多個虛擬機器例項,但是Delvik環境下每次執行應用都需要通過及時編譯器(JIT)將位元組碼轉為機器碼,這雖然安裝過程比較快,但是會拖慢應用每次啟動的效率,並且會重複的JIT(Just-In-Time)會增加CPU的工作造成損耗。於是,2014年6月Google IO大會推出ART(Android Runtime)代替Delvik,在ART環境下應用安裝時,通過預編譯(AOT:Ahead-Of-Time)將位元組碼轉為機器碼,在溢位解釋程式碼這一過程後,這樣就提高了每次應用啟動的執行效率,減少CPU的重複耗損工作。但是是以空間換時間,可能會增加10%-20%的儲存空間和更長時間的應用安裝。

DVM類載入機制

主要是由BaseDexClassLoader的兩個子類 PathClassLoader、DexClassLoader 來完成。繼承自Java的ClassLoader。

PathClassLoader :用來載入系統類和應用類。只能載入已安裝的apk。

DexClassLoader  :用來載入jar、apk、dex檔案。從SD卡中載入未安裝的apk。

PathClassLoader.Class 原始碼

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

DexClassLoader.Class 原始碼

public class DexClassLoader extends BaseDexClassLoader {

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

BaseDexClassLoader.Class 原始碼   

public class BaseDexClassLoader extends ClassLoader {
    private final DexPathList pathList;

    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        //這裡建立了一個DexPathList物件例項
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }

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

三、熱修復原理

   分析上面DVM載入類機制的BaseDexClassLoader.Class 程式碼  ,它類中程式碼:

   首先看到聲明瞭DexPathList類名為pathList,然後看它的建構函式中建立了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);
        ... 
    }

可以看到,DexPathList的建構函式中建立一個dexElements 陣列。

然後再回頭看,BaseDexClassLoader.Class 程式碼的findClass方法。呼叫了pathList.findClass。

/* package */final class DexPathList {
    ...
    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 陣列。然後初始化DexFile,如果DexFile不為空那麼呼叫DexFile類的loadClassBinaryName方法返回Class例項。總而言之,ClassLoader會遍歷這個陣列,然後載入這個陣列中的dex檔案, 而ClassLoader在載入到正確的類之後,就不會再去載入有Bug的那個類了,我們把這個正確的類放在Dex檔案中,讓這個Dex檔案排在dexElements陣列前面即可。

但是,這裡有個問題:

如果引用者和被引用者的類(直接引用關係)在同一個Dex時,那麼在虛擬機器啟動時,被引用類就會被打上CLASS_ISPREVERIFIED標誌。這樣被引用的類就不能進行熱修復操作了。怎麼辦?

原因是:那麼我們就要阻止被引用類打上CLASS_ISPREVERIFIED標誌,QQ空間的方法是在所有引用到該類的建構函式中插入一段程式碼,程式碼引用到別的類。這個可參考QQ空間團隊的安卓App熱補丁動態修復技術介紹

為了實現補丁方案,防止類被打上CLASS_ISPREVERIFIED標誌。最終QQ空間的方案是往所有類的建構函式裡面插入了一段程式碼,程式碼如下:

if (ClassVerifier.PREVENT_VERIFY) {

System.out.println(AntilazyLoad.class);

}

其中AntilazyLoad類會被打包成單獨的hack.dex,這樣當安裝apk的時候,classes.dex內的類都會引用一個在不相同dex中的AntilazyLoad類,這樣就防止了類被打上CLASS_ISPREVERIFIED的標誌了,只要沒被打上這個標誌的類都可以進行打補丁操作。