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)
- Bootstrap ClassLoader :啟動類載入器,負責載入java基礎類,對應的檔案是%JRE_HOME/lib/ 目錄下的rt.jar、resources.jar、charsets.jar和class等;
- Extension ClassLoader:擴充套件類載入器,對應的檔案是 %JRE_HOME/lib/ext 目錄下的jar和class等;
- 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); } }
輸出結果:
- sun.misc.Launcher$AppClassLoader@1bbf1ca
- sun.misc.Launcher$ExtClassLoader@1ff0dde
- 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的標誌了,只要沒被打上這個標誌的類都可以進行打補丁操作。