Android 熱修復原理篇及幾大方案比較
我們知道Android系統也是仿照java搞了一個虛擬機器,不過它不叫JVM,它叫Dalvik/ART VM他們還是有很大區別的(這是不是我們的重點,
點開是個拓展閱讀)。我們只需要知道,Dalvik/ART VM 虛擬機器載入類和資源也是要用到ClassLoader
,不過Jvm通過ClassLoader
載入的class位元組碼,而Dalvik/ART
VM通過ClassLoader
載入則是dex。
Android的類載入器分為兩種,PathClassLoader和DexClassLoader,兩者都繼承自BaseDexClassLoader
PathClassLoader程式碼位於libcore\dalvik\src\main\
DexClassLoader程式碼位於libcore\dalvik\src\main\java\dalvik\system\DexClassLoader.java
BaseDexClassLoader程式碼位於libcore\dalvik\src\main\java\dalvik\system\BaseDexClassLoader.java
- PathClassLoader
-
用來載入系統類和應用類
-
DexClassLoader
用來載入jar、apk、dex檔案.載入jar、apk也是最終抽取裡面的Dex檔案進行載入.
2.熱修復機制
熱修復就是利用dexElements
的順序來做文章,當一個補丁的patch.dex放到了dexElements
的第一位,那麼當載入一個bug類時,發現在patch.dex中,則直接載入這個類,原來的bug類可能就被覆蓋了
看下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);
}
}
DexClassLoader程式碼
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}
兩個ClassLoader就兩三行程式碼,只是呼叫了父類的建構函式.
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
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;
}
在BaseDexClassLoader 建構函式中建立一個DexPathList類的例項,這個DexPathList的建構函式會建立一個dexElements 陣列
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);
...
}
然後BaseDexClassLoader 重寫了findClass方法,呼叫了pathList.findClass,跳到DexPathList類中.
/* 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;
}
...
}
會遍歷這個陣列,然後初始化DexFile,如果DexFile不為空那麼呼叫DexFile類的loadClassBinaryName方法返回Class例項.
歸納上面的話就是:ClassLoader會遍歷這個陣列,然後載入這個陣列中的dex檔案.
而ClassLoader在載入到正確的類之後,就不會再去載入有Bug的那個類了,我們把這個正確的類放在Dex檔案中,讓這個Dex檔案排在dexElements陣列前面即可.
CLASS_ISPREVERIFIED問題
根據QQ空間談到的在虛擬機器啟動的時候,在verify選項被開啟的時候,如果static方法、private方法、建構函式等,其中的直接引用(第一層關係)到的類都在同一個dex檔案中,那麼該類就會被打上CLASS_ISPREVERIFIED標誌,且一旦類被打上CLASS_ISPREVERIFIED標誌其他dex就不能再去替換這個類。所以一定要想辦法去阻止類被打上CLASS_ISPREVERIFIED標誌。
為了阻止類被打上CLASS_ISPREVERIFIED標誌,QQ空間開發團隊提出了一個方法是先將一個預備好的hack.dex加入到dexElements
的第一項,讓後面的dex的所有類都引用hack.dex其中的一個類,這樣原來的class1.dex、class2.dex、class3.dex中的所有類都引用了hack.dex的類,所以其中的都不會打上CLASS_ISPREVERIFIED標誌。
比如Qzon團隊的 安卓App熱補丁動態修復技術介紹 (這個一定要看!!! 他是熱修復元老級文章,也是重點抄襲物件)
動態載入class檔案,然後呼叫反射完成修復的原理:Java程式在執行的時候,JVM通過類載入機制(ClassLoader)把class檔案載入到記憶體中,只有class檔案被載入記憶體,才能被其他class引用,使程式正確執行起來.Java中的ClassLoader有三種. 1. Bootstrap ClassLoader 由C++寫的,由JVM啟動. 啟動類載入器,負責載入java基礎類,對應的檔案是%JRE_HOME/lib/ 目錄下的rt.jar、resources.jar、charsets.jar和class等 2.Extension ClassLoader Java類,繼承自URLClassLoader 擴充套件類載入器,對應的檔案是 %JRE_HOME/lib/ext 目錄下的jar和class等 3.App ClassLoader
Java類,繼承自URLClassLoader 系統類載入器,對應的檔案是應用程式classpath目錄下的所有jar和class等
這裡要注意一點:只有被同一個類載入器例項載入並且檔名相同的class檔案才被認為是同一個class. 下面來一個小例子: 因為系統的ClassLoader只會載入指定目錄下的class檔案,如果你想載入自己的class檔案,那麼就可以自定義一個ClassLoader.\ 如何自定義ClassLoader 新建一個類繼承自java.lang.ClassLoader,重寫它的findClass方法。--將class位元組碼陣列轉換為Class類的例項---呼叫loadClass方法即可
我先建一個叫Log的類,很簡單,只有一句列印
|
業界內比較著名的有阿里巴巴的AndFix,HotFix(內測)、Dexposed,Qzone的超級補丁和tencent的Tinker(將開源)以及大眾點評的Nuwa,騰訊Bugly,RocooFix等
Dex的熱修復總結
Dex的熱修復目前來看基本上有四種方案:
- 阿里系的從native層入手,見AndFix
- 微信的方案,見微信Android熱補丁實踐演進之路,dexDiff和dexPatch,方法很牛逼,需要全量插入,但是這個全量插入的dex中需要刪除一些過早載入的類,不然同樣會報class is pre verified異常,還有一個缺點就是合成佔記憶體和內建儲存空間。微信讀書的方式和微信類似,見Android Patch 方案與持續交付,不過微信讀書是miniloader方式,啟動時容易ANR,在我錘子手機上變現出來特別明顯,長時間的卡圖示現象。
此外,微信的方案是多classloader,這種方式可以解決用multidex方式在部分機型上不生效patch的問題,同時還帶來一個好處,這種多classloader的方式使用的是instant run的程式碼,如果存在native library的修復,也會帶來極大的方便。
Native Library熱修復總結
而native libraray的修復,目前來說,基本上有兩種方案。。
- 類似multidex的dex方式,插入目錄到陣列最前面,具體文章見Android熱更新之so庫的熱更新,需要處理系統的相容性問題,系統分隔線是Android 6.0
- 第二種方式需要依賴多classloader,在構造BaseDexClassLoader的時候,獲取原classloader的native library,通過環境變數分隔符(冒號),將patch的native library與原目錄進行連線,patch目錄在前,這樣同樣可以達到修復的目的,缺點是需要依賴dex的熱修復,優點是應用native library時不需要處理相容性問題,當然從patch中釋放出來的時候也需要處理相容性問題。
上述方案從原理上可以簡單劃分為3類:
原理 | 方案 |
---|---|
Native hook方案 | AndFix |
QQ空間提出的Classloader替換類的方案 | Nuwa, HotFix, RocooFix |
Instant Run的冷插拔原理的Dex替換 | Tinker |
優缺點分析
測試模組 | AndFix | Classloader方案 | Tinker |
---|---|---|---|
類替換 | no | yes | yes |
資源替換 | no | no | yes |
是否需要重啟 | no | yes | yes |
相容穩定性 | 不穩定 | 最好 | 穩定 |
下面,我們就分別介紹QQ空間超級熱補丁技術和微信Tinker以及阿里百川的HotFix技術。
一、Qzone超級補丁技術
超級補丁技術基於DEX分包方案,使用了多DEX載入的原理,大致的過程就是:把BUG方法修復以後,放到一個單獨的DEX裡,插入到dexElements陣列的最前面,讓虛擬機器去載入修復完後的方法。
當patch.dex中包含Test.class時就會優先載入,在後續的DEX中遇到Test.class的話就會直接返回而不去載入,這樣就達到了修復的目的。
但是有一個問題是,當兩個呼叫關係的類不在同一個DEX時,就會產生異常報錯。我們知道,在APK安裝時,虛擬機器需要將classes.dex優化成odex檔案,然後才會執行。在這個過程中,會進行類的verify操作,如果呼叫關係的類都在同一個DEX中的話就會被打上`CLASS_ISPREVERIFIED`的標誌,然後才會寫入odex檔案。
所以,為了可以正常地進行打補丁修復,必須避免類被打上`CLASS_ISPREVERIFIED`標誌,具體的做法就是單獨放一個類在另外DEX中,讓其他類呼叫。
我們來逆向手機QQ空間APK看一下具體的實現:
先進入程式入口`QZoneRealApplication`,在`attachBaseContext`中進行了兩步操作:修復`CLASS_ISPREVERIFIED`標誌導致的unexpected DEX problem異常、載入修復的DEX。
1. 修復Unexpected DEX Problem異常
先看程式碼,
可以看到,這裡是要載入一個libs目錄下的dalvikhack.jar。在專案的assets/libs找到該檔案,解壓得到’classes.dex’檔案,逆向開啟該DEX檔案,
通過不同的DEX載入進來,然後在每一個類的構造方法中引用其他DEX中的唯一類AnitLazyLoad,避免類被打上CLASS_ISPREVERIFIED標誌。
在無修復的情況下,將DO_VERIFY_CLASSES設定為false,以提高效能。只有在需要修復的時候,才設定為true。
至於如何載入進來,與下面第二個步驟基本相同。
2. 載入修復的DEX
從loadPatchDex()方法進入,經過幾次跳轉,到達核心的程式碼段,`SystemClassLoaderInjector.c()`。由於進行了混淆和多次方法的跳轉,於是將核心程式碼段做了如下整理:
修復的步驟為:
1. 可以看出是通過獲取到當前應用的Classloader,即為BaseDexClassloader
2. 通過反射獲取到他的DexPathList屬性物件pathList
3. 通過反射呼叫pathList的dexElements方法把patch.dex轉化為Element[]
4. 兩個Element[]進行合併,把patch.dex放到最前面去
5. 載入Element[],達到修復目的
整體的流程圖如下:
從流程圖來看,可以很明顯的找到這種方式的特點:
優勢:
- 沒有合成整包(和微信Tinker比起來),產物比較小,比較靈活
- 可以實現類替換,相容性高。(某些三星手機不起作用)
不足:
1. 不支援即時生效,必須通過重啟才能生效。
2. 為了實現修復這個過程,必須在應用中加入兩個dex!dalvikhack.dex中只有一個類,對效能影響不大,但是對於patch.dex來說,修復的類到了一定數量,就需要花不少的時間載入。對手淘這種航母級應用來說,啟動耗時增加2s以上是不能夠接受的事。
3. 在ART模式下,如果類修改了結構,就會出現記憶體錯亂的問題。為了解決這個問題,就必須