1. 程式人生 > >阿裏最新熱修復Sophix與QQ超級補丁和Tinker的實現與總結

阿裏最新熱修復Sophix與QQ超級補丁和Tinker的實現與總結

files 透明度 完全 修改 請求 圖片 依賴 完成 業務

2015年以來,Android開發領域裏對熱修復技術的討論和分享越來越多,同時也出現了一些不同的解決方案,如QQ空間補丁方案、阿裏AndFix以及微信Tinker(Bugly sdk也集成Tikner熱更新)和阿裏最新出品Sophix.它們在原理各有不同,適用場景各異,到底采用哪種方案,是開發者比較頭疼的問題。下面是這幾種技術方案介紹。

技術背景

技術分享圖片

一、正常開發流程

從流程來看,傳統的開發流程存在很多弊端:

·重新發布版本代價太大

·用戶下載安裝成本太高

·

BUG修復不及時,用戶體驗太差

二、熱修復開發流程

技術分享圖片

而熱修復的開發流程顯得更加靈活,優勢很多:

·無需重新發版,實時高效熱修復

·用戶無感知修復,無需下載新的應用,代價小

·修復成功率高,把損失降到最低

業界熱門的熱修復技術

熱修復作為當下熱門的技術,在業界內比較著名的有阿裏巴巴的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中,讓其他類調用。

修復的步驟為:

1.可以看出是通過獲取到當前應用的Classloader,即為BaseDexClassloader

2.通過反射獲取到他的DexPathList屬性對象pathList

3.通過反射調用pathList的dexElements方法把patch.dex轉化為Element[]

4.兩個Element[]進行合並,把patch.dex放到最前面去

5.加載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文件,以達到修復的目的。

技術分享圖片

整體的流程如下:

技術分享圖片

從流程圖來看,同樣可以很明顯的找到這種方式的特點:

優勢:

1.合成整包,不用在構造函數插入代碼,防止verify,verify和opt在編譯期間就已經完成,不會在運行期間進行。

2.性能提高。兼容性和穩定性比較高。

3.開發者透明,不需要對包進行額外處理。

不足:

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層操作,經過三個步驟:

技術分享圖片

AndFix對ART設備支持,過程與Dalvik相似。

從技術原理,不難看出阿裏百川HotFix的幾個特點:

優勢:

1.

BUG修復的即時性

2.補丁包同樣采用差量技術,生成的PATCH體積小

3.對應用無侵入,幾乎無性能損耗

不足:

1.不支持新增字段,以及修改方法,也不支持對資源的替換。

2.由於廠商的自定義ROM,對少數機型暫不支持。兼容性差。

(每一個java方法在art種都對應一個ArtMethod, ArtMethod記錄了這個java方法的所有信息,包括所屬類,訪問權限、代碼執行地址。

通過evn->FromReflectedMethod,可以由Method對象得到這個方法對應的ArtMethod的真正其實地址,然後就可以把它強轉為ArtMethod指針,從而對其所有的成員進行修改。這樣就全部替換完之後就完成了熱修復邏輯。以後調用這個方法時就會直接走到新方法的實現中了。

然而由於andfix裏面的ArtMethod的結構體遵照android虛擬機art源碼裏面的ArtMethod構建的,各個手機廠商對這個ArtMethod結構體進行修改就會導致喝原來開源代碼裏面的結構不一致,那麽在這個修改過的設備上,替換機制就會出問題,無法正常執行熱修復邏輯。)

綜合分析如下:

技術分享圖片

對比總結:

一、qq空間超級補丁,微信Tinker類似多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修復的這個使用場景上還不夠明確,並且顯得過重。在插件化開發上,有用武之地。

同樣andfix兼容又有很大的問題。

終於進入主題阿裏最新推出的熱修復方案技術sophfix.

Sophix設計理念:

Sophix的核心設計理念,就是非侵入性。

我們的打包過程不會侵入到apk的build流程中。我們所需要的,只有已經生成完畢的新舊apk,而至於apk是如何生成的——是Android

Studio打包出來的、還是Eclipse打包出來的、或者是自定義的打包流程,我們一律不關心。在生成補丁的過程中間既不會改變任何打包組件,也不插入任何AOP代碼,我們極力做到了——不添加任何超出開發者預期的代碼,以避免多余的熱修復代碼給開發者帶來困擾。

在Sophix中,唯一需要的就是初始化和請求補丁兩行代碼,甚至連入口Application類我們都不做任何修改,這樣就給了開發者最大的透明度和自由度。我們甚至重新開發了打包工具,使得補丁工具操作圖形界面化,這種所見即所得的補丁生成方式也是阿裏熱修復獨家的。因此,Sophix的接入成本也是目前市面上所有方案裏最低的。

這種非侵入式熱更新理念,是我們在設計過程中從用戶使用角度進行了深入思考而提煉出的核心思想。

這裏的用戶,指的自然是廣大的開發者。對於開發者而言,熱修復應該是一個與業務無關的SDK組件,在整個開發過程中感知不到它的存在。最理想的情況,就是開發者拿過來兩個apk,一個是已經安裝在手機上的apk,另一個是將要發布出去的apk。我們直接通過工具,就可以根據這兩個apk生成補丁,然後把這個補丁下發給已經安裝的舊app上,就可以直接加載,使舊app重生為新的app。而這個加載了補丁包新app,在功能和使用上,將會和直接安裝新apk別無二致。

Sophfix與其他熱修復方案對比:

技術分享圖片

可以看到,Sophix在各個指標上全面占優。而其中唯一不支持的地方就是四大組件的修復,這是因為如果要修復四大組件,必須在AndroidManifest裏面預先插入代理組件,並且盡可能聲明所有權限,而這麽做就會給原先的app添加很多臃腫的代碼,對app運行流程的侵入性很強。

Sophix支持代碼修復、資源修復、so庫修復。下面對這三種修復進行介紹。

、代碼修復

代碼修復有兩大主要方案,一種是阿裏系的底層替換方案,另一種是騰訊系的類加載方案。

這兩類方案各有優劣:

底層替換方案限制頗多,但時效性最好,加載輕快,立即見效。

類加載方案時效性差,需要重新冷啟動才能見效,但修復範圍廣,限制少。

底層替換方案

底層替換方案是在已經加載了的類中直接替換掉原有方法,是在原來類的基礎上進行修改的。因而無法實現對與原有類進行方法和字段的增減,因為這樣將破壞原有類的結構。

一旦補丁類中出現了方法的增加和減少,就會導致這個類以及整個Dex的方法數的變化。方法數的變化伴隨著方法索引的變化,這樣在訪問方法時就無法正常地索引到正確的方法了。

如果字段發生了增加和減少,和方法變化的情況一樣,所有字段的索引都會發生變化。並且更嚴重的問題是,如果在程序運行中間某個類突然增加了一個字段,那麽對於原先已經產生的這個類的實例,它們還是原來的結構,這是無法改變的。而新方法使用到這些老的實例對象時,訪問新增字段就會產生不可預期的結果。

這是這類方案的固有限制,而底層替換方案最為人詬病的地方,在於底層替換的不穩定性。

傳統的底層替換方式,不論是Dexposed、Andfix或者其他安全界的Hook方案,都是直接依賴修改虛擬機方法實體的具體字段。例如,改Dalvik方法的jni函數指針、改類或方法的訪問權限等等。這樣就帶來一個很嚴重的問題,由於Android是開源的,各個手機廠商都可以對代碼進行改造,而Andfix裏ArtMethod的結構是根據公開的Android源碼中的結構寫死的。如果某個廠商對這個ArtMethod結構體進行了修改,就和原先開源代碼裏的結構不一致,那麽在這個修改過了的設備上,通用性的替換機制就會出問題。這便是不穩定的根源。

而我們也對代碼的底層替換原理重新進行了深入思考,從克服其限制和兼容性入手,以一種更加優雅的替換思路,實現了即時生效的代碼熱修復。sophix實現的是一種無視底層具體結構的替換方式,也就是把原先這樣的逐一替換:

技術分享圖片

變成了這樣的整體替換:

技術分享圖片

這麽一來,我們不僅解決了兼容性問題,並且由於忽略了底層ArtMethod結構的差異,對於所有的Android版本都不再需要區分,代碼量大大減少。即使以後的Android版本不斷修改ArtMethod的成員,只要保證ArtMethod數組仍是以線性結構排列,就能直接適用於將來的Android 8.0、9.0等新版本,無需再針對新的系統版本進行適配了。

事實也證明確實如此,當我們拿到Google剛發不久的Android O(8.0)開發者預覽版的系統時,hotfix demo直接就能順利地加載補丁跑起來了,我們並沒有做任何適配工作,穩定性極好。

類加載方案

類加載方案的原理是在app重新啟動後讓Classloader去加載新的類。因為在app運行到一半的時候,所有需要發生變更的類已經被加載過了,在Android上是無法對一個類進行卸載的。如果不重啟,原來的類還在虛擬機中,就無法加載新類。因此,只有在下次重啟的時候,在還沒走到業務邏輯之前搶先加載補丁中的新類,這樣後續訪問這個類時,就會Resolve為新類。從而達到熱修復的目的。

再來看看騰訊系三大類加載方案的實現原理。QQ空間方案會侵入打包流程,並且為了hack添加一些無用的信息,實現起來很不優雅。而QFix的方案,需要獲取底層虛擬機的函數,不夠穩定可靠,並且有個比較大的問題是無法新增public函數。

微信的Tinker方案是完整的全量dex加載,並且可謂是將補丁合成做到了極致,然而我們發現,精密的武器並非適用於所有戰場。Tinker的合成方案,是從dex的方法和指令維度進行全量合成,整個過程都是自己研發的。

雖然可以很大地節省空間,但由於對dex內容的比較粒度過細,實現較為復雜,性能消耗比較嚴重。實際上,dex的大小占整個apk的比例是比較低的,一個app裏面的dex文件大小並不是主要部分,而占空間大的主要還是資源文件。因此,Tinker方案的時空代價轉換的性價比不高。

其實,dex比較的最佳粒度,應該是在類的維度。它既不像方法和指令維度那樣的細微,也不像bsbiff比較那般的粗糙。在類的維度,可以達到時間和空間平衡的最佳效果。基於這個準則,我們另辟蹊徑,實現了一種完全不同的全量dex替換方案。

sophix采用的也是全量合成dex的技術,這個技術是從手淘插件化框架Atlas汲取的。直接利用Android原先的類查找和合成機制,快速合成新的全量dex。這麽一來,我們既不需要處理合成時方法數超過的情況,對於dex的結構也不用進行破壞性重構。

技術分享圖片

從圖中可以看到,我們重新編排了包中dex的順序。這樣,在虛擬機查找類的時候,會優先找到classes.dex中的類,然後才是classes2.dex、classes3.dex,也可以看做是dex文件級別的類插樁方案。這個方式十分巧妙,它對舊包與補丁包中classes.dex的順序進行了打破與重組,最終使得系統可以自然地識別到這個順序,以實現類覆蓋的目的。這將會大大減少合成補丁的開銷。

雙劍合璧

既然底層替換方案和類加載方案各有其優點,把他們聯合起來不是最好的選擇嗎?Sophix的代碼修復體系正是同時涵蓋了這兩種方案。兩種方案的結合,可以實現優勢互補,完全兼顧的作用,可以靈活地根據實際情況自動切換。

這兩種方案我們都進行了重大的改進,並且從補丁生成到應用的各個環節都進行了研究,使得二者能很好地整合在一起。在補丁生成階段,補丁工具會根據實際代碼變動情況進行自動選擇,針對小修改,在底層替換方案限制範圍內的,就直接采用底層替換修復嗎,這樣可以做到代碼修復即時生效。而對於代碼修改超出底層替換限制的,會使用類加載替換,這樣雖然及時性沒那麽好,但總歸可以達到熱修復的目的。

另外,運行時階段,Sophix還會再判斷所運行的機型是否支持熱修復,這樣即使補丁支持熱修復,但由於機型底層虛擬機構造不支持,還是會走類加載修復,從而達到最好的兼容性。最後也要註意

、資源修復

不android資源熱修復,就是在app不重新安裝的情況下,利用下發補丁包直接更新本app中的資源。

目前市面上的資源熱修復方案基本上都是參考Instant Run的實現。Instant Run實現過程大概分為兩部:

1、構造一個新的AssetManager,並通過反射條用addAssetPath,把這個完整的新資源包加入到AssetManager中。這樣就得到了一個含有所有新資源的AssetManager。

2、找到所有之前引用到原油AssetManager的地方,通過反射,把引用處替換為AssetManager

這種方式下發完整的包很占用空間。而像有些方案,是先進行對資源包做差量,在運行時合成完整包再加載。這樣確實減少包的體積,但是在運行時多了合成的操作,耗費了運行時間喝內存。合成後的包也是完整的包,仍舊會占磁盤空間。

Sophix采用的一種很巧妙的方式,構造一個package id為0x66的資源包,這個包裏面只包含改變了的資源項,然後直接在原來的AssetManager中addAssetPaht這個包。然後,就可以了。憂郁補丁包的package id為0x66和原來文件的package id為0x7f不沖突,因此直接加入到一有的AssetManager中就直接使用了。補丁包裏面的資源,只包含原油包裏面沒有而新包裏有的資源以及內容發生改變的資源。

資源的改變包含增加、減少、修改,分別處理方法如下:

1、新增資源,直接加入布丁包

2、減少的資源,我們只要不使用就行了,因此不用考慮這種情況,它不影響布丁包

3、對於修改資源,比如替換了一張圖片之類的情況,我們把他視為新增資源,在打入補丁的時候,代碼在引用出也會做相應的修改,也就是直接把原來使用的舊資源id的地方變為新id。

Sophix優勢:

1.不修改AssetManager的引用處,替換更快更完全。(對比Instanat Run以及所有copycat的實現)

2.不必下發完整包,補丁包中只包含有變動的資源。(對比Instanat Run、Amigo等方式的實現)

3.不需要在運行時合成完整包。不占用運行時計算和內存資源。(對比Tinker的實現)

三、so庫修復

so庫的修復本質上是對native方法的修復和替換。

我們知道JNI編程中,native方法可以通過動態註冊和靜態註冊兩種方式進行。動態註冊的native方法必須實現`JNI_OnLoad`方法,同時實現一個`JNINativeMethod[]`數組,靜態註冊的native方法必須是`Java+類完整路徑+方法名`的格式。

技術分享圖片

動態註冊的native方法映射通過加載so庫過程中調用JNI_OnLoad方法調用完成,靜態註冊的native方法映射是在該native方法第一次執行的時候才完成映射,當然前提是該so庫已經load過。

我們采用的是類似類修復反射註入方式。把補丁so庫的路徑插入到nativeLibraryDirectories數組的最前面,就能夠達到加載so庫的時候是補丁so庫,而不是原來so庫的目錄,從而達到修復的目的。

技術分享圖片

采用這種方案,完全由Sophix在啟動期間反射註入patch中的so庫。對開發者依然是透明的。不用像某些其他方案需要手動替換系統的System.load來實現替換目的。



作者:日月星辰_9e1a
鏈接:https://www.jianshu.com/p/0a31d145cad2
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權並註明出處。

阿裏最新熱修復Sophix與QQ超級補丁和Tinker的實現與總結