我們知道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\Java\dalvik\system\PathClassLoader.java
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
- QQ空間的方案,插樁,見安卓App熱補丁動態修復技術介紹
- 微信的方案,見微信Android熱補丁實踐演進之路,dexDiff和dexPatch,方法很牛逼,需要全量插入,但是這個全量插入的dex中需要刪除一些過早加載的類,不然同樣會報class is pre verified異常,還有一個缺點就是合成佔內存和內置存儲空間。微信讀書的方式和微信類似,見Android Patch 方案與持續交付,不過微信讀書是miniloader方式,啓動時容易ANR,在我錘子手機上變現出來特別明顯,長時間的卡圖標現象。
- 美團的方案,也就是instant run的方案,見Android熱更新方案Robust
此外,微信的方案是多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模式下,如果類修改了結構,就會出現內存錯亂的問題。爲了解決這個問題,就必須把所有相關的調用類、父類子類等等全部加載到patch.dex中,導致補丁包異常的大,進一步增加應用啓動加載的時候,耗時更加嚴重。
二、微信Tinker
微信針對QQ空間超級補丁技術的不足提出了一個提供DEX差量包,整體替換DEX的方案。主要的原理是與QQ空間超級補丁技術基本相同,區別在於不再將patch.dex增加到elements數組中,而是差量的方式給出patch.dex,然後將patch.dex與應用的classes.dex合併,然後整體替換掉舊的DEX文件,以達到修復的目的。
我們來逆向微信的APK看一下具體的實現:
先找到應用入口`TinkerApplication`,在`onBaseContextAttached()`調用了`loadTinker()`,
進入TinkerLoader的tryLoad()方法中,
從方法名可以預見,在tryLoadPatchFilesInternal()中嘗試加載本地的補丁,再經過跳轉進入核心修復功能類SystemClassLoaderAdder.class中。
代碼中可以看出,根據Android版本的不同,分別採取具體的修復操作,不過原理都是一樣的。我們以V19爲例,
從代碼中可以看到,通過反射操作得到PathClassLoader的DexPatchList,反射調用patchlist的makeDexElements()方法吧本地的dex文件直接替換到Element[]數組中去,達到修復的目的。
對於如何進行patch.dex與classes.dex的合併操作,這裏微信開啓了一個新的進程,開啓新進程的服務TinkerPatchService進行合併。
整體的流程如下:
從流程圖來看,同樣可以很明顯的找到這種方式的特點:
優勢:
- 合成整包,不用在構造函數插入代碼,防止verify,verify和opt在編譯期間就已經完成,不會在運行期間進行。
- 性能提高。兼容性和穩定性比較高。
- 開發者透明,不需要對包進行額外處理。
不足:
1. 與超級補丁技術一樣,不支持即時生效,必須通過重啓應用的方式才能生效。
2. 需要給應用開啓新的進程才能進行合併,並且很容易因爲內存消耗等原因合併失敗。
3. 合併時佔用額外磁盤空間,對於多DEX的應用來說,如果修改了多個DEX文件,就需要下發多個patch.dex與對應的classes.dex進行合併操作時這種情況會更嚴重,因此合併過程的失敗率也會更高。
三、阿里百川HotFix
阿里百川推出的熱修復HotFix服務,相對於QQ空間超級補丁技術和微信Tinker來說,定位於緊急BUG修復的場景下,能夠最及時的修復BUG,下拉補丁立即生效無需等待。
1、AndFix實現原理
AndFix不同於QQ空間超級補丁技術和微信Tinker通過增加或替換整個DEX的方案,提供了一種運行時在Native修改Filed指針的方式,實現方法的替換,達到即時生效無需重啓,對應用無性能消耗的目的。
原理圖如下:
2、AndFix實現過程
對於實現方法的替換,需要在Native層操作,經過三個步驟:
接下來以Dalvik設備爲例,來分析具體的實現過程:
1、setup()
對於Dalvik來說,遵循JIT即時編譯機制,需要在運行時裝載libdvm.so動態庫,獲取以下內部函數:
1) dvmThreadSelf( ):查詢當前的線程;
2)dvmDecodeIndirectRef( ):根據當前線程獲得ClassObject對象。
2、setFieldFlag
該操作的目的:把 private、protected的方法和字段都改爲public,這樣纔可被動態庫看見並識別,因爲動態庫會忽略非public屬性的字段和方法。
3、replaceMethod
該步驟是方法替換的核心,替換的流程如下:
AndFix對ART設備同樣支持,具體的過程與Dalvik相似,這裏不再贅述。
從技術原理,不難看出阿里百川HotFix的幾個特點:
優勢:
- BUG修復的即時性
- 補丁包同樣採用差量技術,生成的PATCH體積小
- 對應用無侵入,幾乎無性能損耗
不足:
- 不支持新增字段,以及修改<init>方法,也不支持對資源的替換。
- 由於廠商的自定義ROM,對少數機型暫不支持。
綜合分析如下:
|
熱修復技術的坑與解
——————————————————————————————————————————————————————————————————————
我們可以看到,QQ空間超級補丁技術和微信Tinker的修復原理都基於類加載,在功能上已經支持類、資源的替換和新增,功能非常強大。既然已經有了這麼強大的熱修復技術,爲什麼阿里百川還要推出自己的熱修復方案HotFix呢?
一、多DEX帶來的性能影響
我們知道,多DEX方案原來是用於解決應用方法數65k的問題,現在google也官方支持了MultiDex的實現方案。超級補丁技術和Tinker卻作爲一種熱修復的方案,平生給應用增加了多個DEX,而多DEX技術最大的問題在於性能上的坑,因此基於這種方案的補丁技術影響應用的性能是無疑的。
1. 啓動加載時間過長
我們可以看到,超級補丁技術和Tinker都選擇在Application的attachBaseContext()進行補丁dex的加載,即時這是加載dex的最佳時機,但是依然會帶來很大的性能問題,首當其衝的就是啓動時間太長。
對於補丁DEX來說,應用啓動時虛擬機會進行dexopt操作,將patch.dex文件轉換成odex文件,這個過程本身非常耗時。而這個過程又要求在主線程中,以同步的方式執行,否則無法成功進行修復。就DEX的加載時間,大概做了以下的時間測試。
通過上表可以看到,隨着patch.dex的尺寸增加,在不做任何優化的情況下,啓動時間也直線增長。對於一個應用來說,這簡直是災難性的。
2. 易造成應用的ANR和Crash
由於多DEX加載導致了啓動時間變長,這樣更容易引發應用的ANR。我們知道當應用在主線程等待超過5s以後,就會直接導致長時間無響應而退出。超級補丁技術爲保證ART不出現地址錯亂問題,需要將所有關聯的類全部加入到補丁中,而微信Tinker採取一種差量包合併加載的方式,都會使要加載的DEX體積變得很大。這也很大程度上容易導致ANR情況的出現。
除了應用ANR以外,多DEX模式也同樣很容易導致Crash情況的出現。在ART設備中爲了保證不出現地址錯亂,需要把修改類的所有相關類全部加入到補丁中,這裏會出現一個問題,爲了保證補丁包的體積最小,能否保證引入全部的關聯類而不引入無關的類呢?一旦沒有引入關聯的類,就會出現以下的異常:
- NoClassDefFoundError
- Could Not Find Class
- Could Not Find Method
出現這些異常,就會直接導致應用的Crash退出。
所以,不難看出如果我們需要修復一個不是Crash的BUG,但是因爲未加入相關類而導致了更嚴重的Crash,就更加的得不償失。
總的來說,熱修復本質的目的是爲了保證應用更加穩定,而不是爲了更強大的功能引入更大的風險和不穩定性。
二、 熱修復 or 插件化?
我們經常提到熱修復和插件化,這都是當下熱門的新興技術。在講述之前,需要對這兩個概念進行一下解釋。
- 熱修復:當線上應用出現緊急BUG,爲了避免重新發版,並且保證修復的及時性而進行的一項在線推送補丁的修復方案。
- 插件化:一個程序劃分爲不同的部分,以插件的形式加載到應用中去,本質上它使用的技術還是熱修復技術,只是加入了更多工程實踐,讓它支持大規模的代碼更新以及資源和SO包的更新。
顯然,從概念上我們可以看到,插件化使用場景更多是功能上的,熱修復強調微小的修復。從這個層面來說,插件化必然功能更加強大,能做的事情也更多。QQ空間超級補丁技術和微信Tinker從類、資源的替換和更新上來看,與其說是熱修復,不如說是插件化技術的實踐。
QQ空間超級補丁技術和微信Tinker提供了更加強大的功能,但是對應用的性能和穩定有較大的影響,就BUG修復的這個使用場景上還不夠明確,並且顯得過重。
針對應用的性能損耗,我們可以舉例做一個對比:
某APP的啓動載入時間爲3s左右,本身就是基於多DEX模式的實現。
分別接入三種熱修復服務,根據騰訊提供超級補丁技術和Tinker的數據,那麼會變成以下的場景:
1. 阿里百川HotFix:啓動時間幾乎無增加,不增加運行期額外的磁盤消耗。
2. QQ空間超級補丁技術:如果應用有700個類,啓動耗時增加超過2.5s,達到5.5s以上。
3. 微信Tinker:假設應用有5個DEX文件,分別修改了這5個DEX,產生5個patch.dex文件,就要進行5次的patch合併動作,假設每個補丁1M,那麼就要多佔用7.5M的磁盤空間。
顯然對於修復緊急BUG這個場景,阿里百川HotFix的更爲合適,它更加輕量,可以在不重啓的情況下生效,且對性能幾乎沒有影響。
找了很多資料加上看各種文章源碼寫完這個文章,很多地方的瞭解不是那麼深入,很多東西也是拾人牙慧,希望大家批評指正。