幹貨滿滿,Android熱修複方案介紹
摘要:在雲棲社區技術直播中,阿裏雲客戶端工程師李亞洲(畢言)從技術原理層麵解析和比較了業界幾大熱修複方案,揭開了Qxxx方案、Instant Run以及阿裏Sophix等熱修複方案的神秘麵紗,幫助大家更加深刻地理解了代碼插樁、全量dex替換、資源修複等常見場景解決方案,本文幹貨滿滿,精彩不容錯過。以下內容根據演講視頻以及PPT整理而成。
在傳統的修複模式下,如果線上的App出現Bug之後進行修複所需要的時間成本非常高,這是因為往往需要發布一個新的版本,然後將其發布到對應的應用商城中,然後通知用戶下載和更新自己的App。但是尤其在Android這樣沒有統一應用市場的環境下,修複周期可能需要以周來計數。而隨著App的業務越來複雜、代碼量越來越大,出現Bug的概率也會越來越高,如果繼續按照傳統的修複模式就很難滿足業務發展的需求。正是因為這樣的現狀,很多Android端開發的同學就開始思考是否有一種在不發版的前提下修複線上Bug的技術,也就是所謂的熱修複技術。通過這幾年的發展,熱修複技術也得到了很大的發展,很多公司也具有了比較成熟的熱修複方案,同時這些熱修複方案也大規模地在生產環境中進行了實踐。本次會選取幾個比較具有代表性的熱修複方案與大家分享。
在本次的分享中主要會講到三種技術方案:Qxxx(化名)方案、Instant Run和Sophix。Qxxx屬於比較早期的修複方案,而現在由於種種原因可能不會成為大家在進行熱修複時的首選方案,但是其技術原理給了我們很大啟發,所以在本次的技術分享中將Qxxx方案放在第一個進行介紹。第二種方案叫做Instant Run,這是所有從事Android開發的同學都比較熟悉的一種方案,嚴格意義上來講Instant Run其實不算是一個熱修複的方案,它僅僅是作為Android Studio來提高開發效率的一個功能而已,但是其背後的技術原理卻和很多修複方案具有很多相通的地方。第三種方案叫做Sophix,它是阿裏巴巴剛剛發布的一種修複方案,它代表著新一代的熱修複方案,Sophix的功能更加強大而且能夠覆蓋的場景也非常廣,所以也是一個比較優秀的方案。
一、Qxxx方案解析
1.1 Qxxx方案原理介紹

1.2 Android端的類加載原理
接下來為大家簡單介紹Android端類的加載原理。其實Android的類加載和Java的類加載比較類似,都是通過ClassLoader類加載器進行加載,唯一的區別就是Android類加載器加載的是dex文件,所以在Android中加載器的基類叫做BaseDexClassLoader,這個類之下會有兩個子類,一個叫做PathClassLoader,它負責加載Android的SDK,當代碼中引用到Android框架的本身類的時候都是通過PathClassLoader進行加載的;而另外一個子類叫做DexClassLoader,這個類就是用於加載業務層麵代碼的加載器。

1.3 類預校驗問題
而實際情況中,做這樣的方案是不行的。直接進行替換就會出現預校驗的問題。


1.4 字節碼注入
針對預校驗問題,有同學可能會認為隻需要在寫代碼的時候引用一些類就可以了,但是這在實際情況下卻是非常困難的,因為本身Android在打包代碼的時候就會盡可能地將相互依賴的類打包在同一個dex裏麵,所以依靠打包方案本身是很難解決這個問題的。但是預校驗問題也不是沒有辦法解決的,解決的思路是當這些類已經被編譯完成之後,在字節碼的層麵去注入一些來自於其他dex的類。

1.5 代碼插樁
但是為什麼慢慢地大家開始覺得Qxxx方案並不好呢?其原因就在於插樁並不是一個非常好的方式,它所帶來的開銷是非常大的。在dex opt的過程中會執行一個驗證的過程,再執行一個優化的過程,最後將dex文件轉成odex文件。因為進行了插樁,所有的類都沒有被打上預校驗的標簽,所以驗證和優化這兩個過程會被放在真正類加載的時候去執行,如果一兩個類在運行的時候進行加載和優化對於App的性能的影響不大,但是現在的App越來越複雜,當有成千上萬的類需要在運行時進行加載和優化的時候,所帶來的開銷就是非常可觀的了。

二、Instant Run方案解析
從嚴格意義上來講,Instant Run其實並不算一個熱修複方案,它隻是一個優化開發效率的機製。在傳統的開發模式中,當在開發的過程中對代碼進行了一些改動就會進行全量的構建,然後將一個完整的App部署到測試機上,之後進行應用重啟,然後就可以看到代碼的變化與運行效果的變化。

2.1 Instant Run打包邏輯

- manifest注入;大家都知道一個Android工程的所有組件都會注冊到manifest文件下,在這部分中,Instant Run會生成一個自己的application,然後將這個application注冊到manifest配置文件裏麵去,也就是說當整個App運行起來的時候,首先執行的就是application這個類,也就是運行的是Instant Run本身的框架,它可以去做一係列準備工作,當這些工作完成之後再去運行業務代碼。
- Instant Run代碼放入主dex;manifest注入之後,會將Instant Run的代碼放入到Android虛擬機第一個加載的dex文件中,包括classes.dex和classes2.dex,這兩個dex文件存放的都是Instant Run本身框架的代碼,而沒有任何業務層的代碼。正是因為以上的原因,當整個App運行起來的時候首先執行的都是Instant Run的代碼。
- 工程代碼插樁——IncretmentalChange;這個插裝裏麵會涉及到具體的IncretmentalChange類。
- 工程代碼放入instantrun.zip;這裏的邏輯是當整個App運行起來之後才回去解壓這個包裏麵的具體工程代碼,運行整個業務邏輯。

- 當bootstrap application啟動之後會首先加載classes.dex和classes2.dex這兩個主dex文件,當這兩個主dex文件啟動之後,就會啟動AppServer服務。這裏可以將AppServer理解為一個服務器,它會與IDE也就是Android Studio建立連接。當連接建立之後,後續在開發的過程中的代碼改動所形成的補丁包都會通過這個連接下發到App上,並且通過AppServer接收,再通過相應的處理使得補丁生效。
- 當完成了第一個步驟之後,會用本身的ClassLoader去加載instantrun.zip包裏麵真正的工程代碼。
- 最後一步,將宿主application替換成真實的realApplication,然後真正地運行自定義application裏麵的邏輯,達到隱藏自身的效果。
2.2 Instant Run熱插拔、溫插拔和冷插拔簡介
當App啟動之後會啟動一個AppServer服務器的連接,當它加載到patch之後會去判斷patch是否能夠進行熱插拔、溫插拔和冷插拔,然後再去做各種方式所對應的事情。

- HotSwap(熱插拔):修改方法實現後代碼可以實時生效,不需要重啟App也不需要重啟activity,隻要加載補丁之後就可以馬上生效。通常情況下,熱插拔隻適用於方法體內部的邏輯改變。
- WarmSwap(溫插拔):主要針對於需要修改或刪除資源的情況。溫插拔不需要重啟App,但是需要重啟當前的activity後才能生效。
- ColdSwap(冷插拔):主要針對於改變了類的結構、繼承關係、實現接口等情況,此時因為類結構本身被改變了,需要重新去加載這個類,所以需要重啟App之後才能生效。
2.3 Instant Run熱插拔(HotSwap)原理解析
首先IDE下發patch,加載到補丁之後,在App層Instant Run的框架會通過AppPatchLoader去找到哪些類需要被修複,當找到需要被修複的類之後再通過反射的手段將類中的$change變量設置為已經修複後的類。這樣當執行MainActivity的onClick方法的時候實際上執行到的是MainActivity&override的onClick方法,從而實現了熱修複。

2.4 Instant Run溫插拔(WarmSwap)原理解析
對於溫插拔而言,需要首先簡單介紹一下資源修複的邏輯。其實對於Android框架比較熟悉的同學都清楚,在每一個activity裏麵都會有一個叫做mResource的變量,這個mResource變量指向一個Resource對象。在Resource對象中會存在一個指向AssetManager的mAsset變量,而AssetManager類才是真正去管理和維護所有對於資源的訪問的具體類。AssetManager類裏麵會有兩個具體成員,一個是framework-res.apk,其是係統自帶的資源,另一個則是App本身的資源,而所有對於資源的訪問最終都會走到AssetManager類中。正是因為這樣的機製,Instant Run就是通過替換AssetManager的方式達到資源修複的效果。


2.5 Instant Run冷插拔(ColdSwap)原理解析
之前提到所有的用戶代碼都被寫到instantrun.zip包裏麵,當代碼結構本身發生了變化之後,可以在把對應的代碼補丁下發到App之上的時候,將對應的patch寫入對應的Instant Run的路徑底下,再重新進行dex opt的過程,之後框架就可以加載對應的類了。

2.6 Instant Run方案總結

- 首先,熱插拔的優勢在於其不需要重啟,隻需要代碼補丁被下發到端上之後就可以實時地看到修複效果。但是熱插拔的劣勢也很明顯,因為使用了插樁,所以其性能的開銷會非常大。
- 其次,對於溫插拔而言,它的優勢是可以實現資源修複,但是其劣勢就是這種方案會下發全量資源包,開銷也是非常大的。
- 最後,對於冷插拔而言,它支持完整類的替換,但是也存在分包的限製,必須要去做切片,當修改了某一個類之後需要把這個類所有所屬的dex類都打成一個新的dex然後下發到端才可以。
三、Sophix方案解析
Sophix是阿裏巴巴剛剛推出的一款無侵入的熱修複方案,本次分享中就為大家揭曉Sophix的神秘麵紗,看看它到底是怎樣實現的。
3.1 Sophix及時修複(Andfix)原理解析
Sophix也是支持及時修複的,在這一點上與Instant Run一樣,對於方法體邏輯的修改可以在App不重啟的情況下進行。Sophix的及時修複方案其實早在阿裏曾經開源的Andfix方案裏麵就已經實現了。
大家都知道所有的類被加載之後,其方法都會被放在方法區,這是Java層麵的概念,其實這些方法區在native層也就是C層麵都會有各自對應的結構體來描述對應的方法以及執行的邏輯。如果某一個類的方法出現了Bug,那麼可以去新建一個類,把修複後的方法放到這個類裏麵,同時把原來那個類的方法的指針指向新方法的方法體就可以實現方法體的替換,從而實現熱修複的效果。

雖然Andfix及時修複方案看上去很美好,也很漂亮,既能夠實現及時修複又沒有使用插樁付出性能上的代價,但是這個方案也存在很大的限製。之前提到了任何虛擬機在native層都會有對應的結構體來描述方法,而在不同的虛擬機上,描述方法的結構體都是不一樣的,所以需要針對不同的Android虛擬機版本去做不同的適配來匹配不同的結構體,這樣一來兼容性的操作就會非常多。大家都很清楚Android端各個廠商都會定義自己的虛擬機,這時候就無法知道方法所對應的結構體內部是什麼樣的,也就無法實現方法的替換了,所以Andfix方案在現實的環境中存在很大的限製,兼容性會受到非常大的挑戰。

但是,雖然可以使用整體複製的方式去做一次性的結構體替換,但是前提是必須要知道方法結構體的尺寸大小,隻有在知道這些之後才能進行替換,這也就是第二個難題,因為不同的虛擬機版本的結構體大小也不同,那麼如何去知道結構體大小呢?對於這一點Sophix方案的解決方法也非常巧妙。

總之,Sophix及時修複的方法是非常巧妙的,既沒有用到插樁,同時又不需要考慮兼容性,在性能層麵和兼容性層麵都具有很好的保障。從及時修複的角度來看,Sophix的確有“四兩撥千斤”的功效。
3.2 Sophix冷啟動修複原理解析
上麵提到的及時修複隻能針對方法體內部結構被修改的場景,而對於類本身結構的改變,及時修複就沒有辦法了,這時候就需要用到冷啟動修複。冷啟動修複就是需要下發一個新的補丁,在補丁中會有一個新的補丁類,在App重啟的時候會優先加載這個補丁類達到去替換原有Bug類的效果。
對於冷啟動修複而言,針對於不同的虛擬機有不同的原則,Android主流的Dalvik和ART兩個虛擬機,它們最大的區別就是是否支持多個dex文件的加載。ART也就是Android 5.0以上的虛擬機本身就支持多個dex文件加載,而Dalvik卻不支持多個dex加載,隻支持一個dex加載,如果需要支持多個dex加載則需要引入multi-dex方案。而Dalvik和ART加載多個dex文件的不同卻決定了它們需要采用不同熱修複方案的原因。

在做冷啟動修複的時候,Sophix的根本原則就是非侵入式,不能對於App本身有任何改造,同時也要保證整個App的性能,所以不能使用插樁的方案,也必須要做到dex的全量替換,重新去執行dex opt過程生成新的odex,再去把dexElement數組進行全量替換,達到加載新的補丁的效果。
ART的冷啟動修複
ART的冷啟動方案是比較簡單的,因為ART本身就支持多個dex加載,當然多個dex加載也是存在一定順序的,首先需要加載classes.dex。正是基於這樣的加載順序,當patch.dex被下發到端上之後,隻需要將其放到第一位,也就是將其文件名改為classes.dex,而將原來的文件名依次後移一位,然後重新執行loadDex的加載過程,生成新的odex並全量替換原有的odex,這樣就可以保證補丁包dex文件被優先加載,ART下的冷啟動修複就是這樣實現的。

Dalvik的冷啟動修複
下表中除了Sophix還列舉了另外兩個方案進行對比來看。

在冷啟動修複下,Sophix方案有一個很簡單的思想就是當發現某些類存在Bug下發新的補丁之後,如果把原有的存在Bug的類從原來所屬的dex摳出來再去執行加載的時候,因為原有的dex文件不再有這些類了,此時就會去從patch.dex文件中加載到它,這樣就可以實現熱修複的效果,並且這樣並不會有預檢驗的問題,從而最大程度地保證了程序性能。所以這個方案中最困難的一點就是如何把以前這些有Bug的類從dex中摳出來,這個問題可能會非常複雜,因為每個類的大小都不一樣,如何將其從連續排列的內存空間中取出來然後再去做移位操作,這樣想起來很複雜。而實際上Sophix使用了一個很巧妙的方式實現這樣的事情。


3.3 Sophix資源修複原理解析
之前在Instant Run裏麵也提到了資源修複,因為資源修複下發的是全量的資源包,所以並不適合在線上的環境中應用。想要在線上環境做資源修複肯定會使用差量的資源包,下發更新過的資源,而Sophix也是這樣做的。

而Sophix的資源修複會涉及到以下三種情況:
- 新增資源導致原有資源id偏移:對比新舊代碼前,將新包中所引用的未修改資源ID修正。
- 引用內容修改的資源:對比新舊代碼前,在新包中將所引用的原有資源ID置為更新後的ID。
- 刪除資源:無需修改。

四、熱修複方案的總結和對比

- Qxxx方案原理比較簡單,通過代碼插樁繞開預校驗問題,此外通過dexElement插入的方式使得帶補丁的dex文件優先加載。其優點在於實現比較簡單,可以修複大部分類層麵的問題。但是同時其問題也是比較突出的,第一點是不支持實時生效,第二點就是全量插樁的方式侵入性非常強,同時性能損耗也是非常大的。
- Instant Run方案原理同樣用到了代碼插樁,而且其插樁比Qxxx方案更加複雜,它還會用到宿主的application做很多準備工作,這之後才會去執行業務代碼,最後它還會去通過AssetManager重建做資源修複工作。優點是它能同時支持方法更新、類更新和資源更新,並且在方法更新的過程中還可以做到及時修複,不需要重啟App。其問題在於還是使用了全量插樁,所以侵入性很強,同時對於性能的損耗也很大,由於在進行修複時需要下發全量資源包,所以開銷非常大,同時也不適合在實際的生產環境中使用。
- Andfix方案的原理是native方法的替換,這個方法很巧妙,並且實現比較簡單,而且可以做到及時生效。但是它不支持類結構的改變,同時因為不同版本虛擬機的方法體結構不同,無法實現兼容性的處理,所以這種方案的兼容性也比較差。
- Sophix方案使用了很多很巧妙的原理實現,首先它還是使用native方法替換,這種方法會比Andfix更加巧妙,它不需要知道方法結構體具體的成員變量,而直接使用整體的替換,隻需要知道方法結構體的大小即可。它使用了很巧妙的方式,通過兩個緊密排列的方法的地址差完成了方法替換即時生效的功能。Sophix使用全量dex替換去完成冷啟動修複場景,而在資源修複的時候使用了差量資源包注入的方式,最大限度地降低了網絡的開銷,隻需要很輕量地把差量資源包下發就可以了。其優點在於同時支持方法更新、類更新和資源更新,而且包括native方法的替換以及資源包的注入等很多實現非常巧妙和優雅,也非常輕量,並且屬於非侵入式的修複。
最後更新:2017-11-03 14:03:48
上一篇:
數據智能助力智慧航空:阿裏雲雙十一特別訪談
下一篇:
Day2&Day3@JavaOne2017
智能攝像頭如何防範被破解確保安全?
【重大更新】阿裏雲APP V4.2 android版支持SSH遠程連接ECS
android的開源電話/通訊/IM聊天項目全集
Ring.velocity:render velocity templates for ring in clojure
6月20日雲棲精選夜讀:阿裏怎麼發工資?自研薪酬管理係統首次曝光
java中文亂碼解決之道(三)—–編碼詳情:偉大的創想—Unicode編碼
微服務,微架構[六]之springboot集成mybatis
獨家視頻教程,玩轉《阿裏巴巴Java開發手冊》P3C掃描插件
以太坊是什麼鬼?!媲美比特幣的加密幣大揭秘
tomcat下的SSH項目移植weblogic常見錯誤解決方案