淺談Android主流熱修復技術

yangxi_001發表於2017-02-08

熱修復

熱修復作為當下熱門的技術,在業界內比較著名的有阿里巴巴的AndFix、Dexposed,騰訊QQ空間的超級補丁技術和微信的Tinker。最近阿里百川推出的HotFix熱修復服務就基於AndFix技術,定位於線上緊急BUG的即時修復,所以AndFix技術這塊我們重點分析阿里百川HotFix。下面,我們就分別介紹QQ空間超級熱補丁技術和微信的Tinker以及阿里百川HotFix技術

qq空間超級補丁方案

超級補丁技術基於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。
這裡寫圖片描述

修復unexpectedDEX problem異常
\
可以看到,這裡是要載入一個libs目錄下的dalvikhack.jar。在專案的assets/libs找到該檔案,解壓得到classes.dex檔案,逆向開啟該DEX檔案
這裡寫圖片描述
通過不同的DEX載入進來,然後在每一個類的構造方法中引用其他dex中的唯一類AnitLazyLoad,避免類被打上CLASS_ISPREVERIFIED標誌
這裡寫圖片描述
在無修復的情況下,將DO_VERIFY_CLASSES設定為false,提高效能。只有在需要修復的時候,才設定為true
這裡寫圖片描述
至於如何載入進來,與接下來第二個步驟基本相同 載入修復的DEX
從loadPatchDex()方法進入,經過幾次跳轉,到達核心的程式碼段,SystemClassLoaderInjector.c()。由於進行了混淆和多次方法的跳轉,於是將核心程式碼段做了如下整理
這裡寫圖片描述
修復的步驟為:
可以看出是通過獲取到當前應用的Classloader,即為BaseDexClassloader 通過反射獲取到他的DexPathList屬性物件pathList 通過反射呼叫pathList的dexElements方法把patch.dex轉化為Element[] 兩個Element[]進行合併,把patch.dex放到最前面去 載入Element[],達到修復目的
這裡寫圖片描述

優勢
1. 沒有合成整包(和微信Tinker比起來),產物比較小,比較靈活
2. 可以實現類替換,相容性高。(某些三星手機不起作用)

不足
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進行合併

這裡寫圖片描述

這裡寫圖片描述

優勢
1. 合成整包,不用在建構函式插入程式碼,防止verify,verify和opt在編譯期間就已經完成,不會在執行期間進行
2. 效能提高。相容性和穩定性比較高。
3. 開發者透明,不需要對包進行額外處理

不足
1. 與超級補丁技術一樣,不支援即時生效,必須通過重啟應用的方式才能生效。
2. 需要給應用開啟新的程式才能進行合併,並且很容易因為記憶體消耗等原因合併失敗。
3. 佔用額外磁碟空間,對於多DEX的應用來說,如果修改了多個DEX檔案,就需要下發多個patch.dex與對應的classes.dex進行合併操作時這種情況會更嚴重,因此合併過程的失敗率也會更高。

阿里百川HotFix

阿里百川推出的熱修復HotFix服務,相對於QQ空間超級補丁技術和微信Tinker來說,定位於緊急bug修復的場景下,能夠最及時的修復bug,下拉補丁立即生效無需等待

AndFix實現原理
AndFix不同於QQ空間超級補丁技術和微信Tinker通過增加或替換整個DEX的方案,提供了一種執行時在Native修改Filed指標的方式,實現方法的替換,達到即時生效無需重啟,對應用無效能消耗的目的
這裡寫圖片描述AndFix實現過程
這裡寫圖片描述
以Dalvik裝置為例,來分析具體的實現過程 setup()
這裡寫圖片描述
對於Dalvik來說,遵循JIT即時編譯機制,需要在執行時裝載libdvm.so動態庫,獲取以下內部函式:
1) dvmThreadSelf( ):查詢當前的執行緒;
2)dvmDecodeIndirectRef():根據當前執行緒獲得ClassObject物件。 setFieldFlag
這裡寫圖片描述
該操作的目的:讓private、protected的方法和欄位可被動態庫看見並識別。原因在於動態庫會忽略非public屬性的欄位和方法 replaceMethod
這裡寫圖片描述

該步驟是方法替換的核心,替換的流程如下
這裡寫圖片描述

優勢
1. BUG修復的即時性
2. 補丁包同樣採用差量技術,生成的PATCH體積小
3. 對應用無侵入,幾乎無效能損耗

不足
1. 不支援新增欄位,以及修改方法,也不支援對資源的替換。
2. 由於廠商的自定義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裝置下不出現地址錯亂問題,需要把修改類的所有相關類全部加入到補丁中,這裡會出現一個問題,為了保證補丁包的體積最小,能否保證引入全部的關聯類而不引入無關的類呢?一旦沒有引入關聯的類,就會出現以下的異常:
1. NoClassDefFoundError
2. Could not find class
3. Could not find method
出現這些異常,就會直接導致應用的Crash退出。
所以,不難看出如果我們需要修復一個不是Crash的BUG,但是因為未加入相關類而導致了更嚴重的Crash,就更加的得不償失。
總的來說,熱修復本質的目的是為了保證應用更加穩定,而不是為了更強大的功能引入更大的風險和不穩定性。

二、熱修復 or 外掛化?

外掛化:一個程式劃分為不同的部分,以外掛的形式載入到應用中去,本質上它使用的技術還是熱修復技術,只是加入了更多工程實踐,讓它支援大規模的程式碼更新以及資源和SO包的更新。

熱修復:當線上應用出現緊急BUG,為了避免重新發版,並且保證修復的及時性而進行的一項線上推送補丁的修復方案。

從概念上我們可以看到,外掛化使用場景更多是功能,熱修復使用常見在於修復。從這個層面來說,外掛化必然功能更加強大,能做的事情也更多。QQ空間超級補丁技術和微信Tinker從類、資源的替換和更新上來看,與其說是熱修復,不如說是外掛化。
當然,強大的功能也就增加了不穩定的因素。比如上文提到的增加啟動時間,導致ANR、Crash的問題。
QQ空間超級補丁技術和微信Tinker提供了更加強大的功能,但是對應用的效能和穩定有較大的影響,就BUG修復的這個使用場景上還不夠明確,並且顯得過重

針對應用的效能損耗,我們可以舉例做一個對比

某APP的啟動載入時間為3s左右,本身就是基於多DEX模式的實現。 分別接入三種熱修復服務,根據騰訊提供超級補丁技術和Tinker的資料,那麼會變成以下的場景: 阿里百川HotFix:啟動時間幾乎無增加,不增加執行期額外的磁碟消耗。 QQ空間超級補丁技術:如果應用有700個類,啟動耗時增加超過2.5s,達到5.5s以上。 微信Tinker:假設應用有5個DEX檔案,分別修改了這5個DEX,產生5個patch.dex檔案,就要進行5次的patch合併動作,假設每個補丁1M,那麼就要多佔用7.5M的磁碟空間。

顯然對於修復緊急BUG這個場景,阿里百川HotFix的更為合適,它更加輕量,可以在不重啟的情況下生效,且對效能幾乎沒有影響。微信Tinker、QQ空間超級補丁技術更多地把場景定位在釋出小的新功能上,採用ClassLoader的模式,犧牲較高的效能代價去實現類、資源新增或替換的功能。阿里百川HotFix對應用本身做到無侵入,無效能損耗

總結

QQ空間超級補丁技術和微信Tinker 支援新增類和資源的替換,在一些功能化的更新上更為強大,但對應用的效能和穩定會有的一定的影響;阿里百川HotFix雖然暫時不支援新增類和資源的替換,對新功能的釋出也有所限制,但是作為一項定位為線上緊急BUG的熱修復的服務來說,能夠真正做到BUG即時修復使用者無感知,同時保證對應用效能不產生不必要的損耗,在熱修復方面不失為一個好的選擇!

相關文章