閱讀333 返回首頁    go 阿裏雲 go 技術社區[雲棲]


幹貨滿滿,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方案原理介紹
fe36f75722f3d5a26f1c58ecabc8f7251495a18e
可能大家對於Qxxx方案比較了解,它也是來自於業界一家非常優秀的公司所提供的方案。首先分享Qxxx方案比較基礎的特性,它是基於Android dex分包方案,其次它最關鍵的技術點在於利用字節碼插樁的方式繞開了預校驗問題,這也是Qxxx方案最為核心的一點。Qxxx隻支持App重啟之後才能修複,也就是App在運行的時候加載到了補丁包也不能及時修複,需要App重新啟動的時候才會修複,這是因為Qxxx方案是基於類加載區需要重新加載補丁類才能實現的,所以必須進行重啟才能修複。此外,Qxxx方案隻支持到類結構本身代碼層麵的修複,不支持資源的修複。

1.2 Android端的類加載原理
接下來為大家簡單介紹Android端類的加載原理。其實Android的類加載和Java的類加載比較類似,都是通過ClassLoader類加載器進行加載,唯一的區別就是Android類加載器加載的是dex文件,所以在Android中加載器的基類叫做BaseDexClassLoader,這個類之下會有兩個子類,一個叫做PathClassLoader,它負責加載Android的SDK,當代碼中引用到Android框架的本身類的時候都是通過PathClassLoader進行加載的;而另外一個子類叫做DexClassLoader,這個類就是用於加載業務層麵代碼的加載器。
af9318c9418dc12bd7903b62c2a2ba47282d466b
早期在Android端隻能加載一個dex文件,後來隨著代碼越來越多,一個dex文件已經無法存放所有代碼了,所以需要加載多個dex文件,這無論是在什麼虛擬機內都可以通過相同的手法解決。而解決的關鍵就是無論有幾個dex文件,所有的dex文件最後最後被加入到內存中都會形成一個數組叫做dexElement。而DexClassLoader的原理就是當係統需要加載某一個類的時候,DexClassLoader就會去遍曆dexElement數組,當它遍曆到某一個元素的時候,在這個元素所對應的文件找到目標類的時候就會停止遍曆並及時返回。所以當在不同的dex文件裏麵出現兩個一樣的類的時候,哪一個被先遍曆到哪個就會被先加載。正是基於這樣的原理,Qxxx方案的研發同學就想到如果編寫的代碼存在Bug,就可以發布一個補丁包,而這個補丁包也是dex文件,並且使這個dex文件能夠首先被DexClassLoader加載到,就能夠達到修複的效果。所以就是需要通過一定的手段將補丁包的dex文件放在dexElement數組中的第一位,那麼就可以首先加載補丁中的類,進而達到修複的效果。

1.3 類預校驗問題
而實際情況中,做這樣的方案是不行的。直接進行替換就會出現預校驗的問題。
33e49660df3fd8d4c14516ef39267ce8fe199610
而預校驗問題出現的原因在於當App被安裝到設備上之後,會存在一個叫做dex opt的過程,也就是將dex文件進行優化,在優化之後dex文件將會變成odex文件,而在優化過程中就會有一個預校驗的問題。當某一個類所有的構造方法、私有方法以及重載方法所引用的其他類和這個類本身都來自於同一個dex文件的時候,這個類就會被打上class_ispreverified標簽。所以如果加載的類來自於補丁文件,而補丁文件和之前的文件必然不屬於同一個dex,而本身的那個類已經被打上了class_ispreverified標簽,但是在運行時又引用了其他dex的類,這樣就必然會出現錯誤。所以Qxxx方案所需要解決的關鍵問題就是預校驗。
49581a82a0c07555e2c4b00ddc906649f1ace69a

1.4 字節碼注入
針對預校驗問題,有同學可能會認為隻需要在寫代碼的時候引用一些類就可以了,但是這在實際情況下卻是非常困難的,因為本身Android在打包代碼的時候就會盡可能地將相互依賴的類打包在同一個dex裏麵,所以依靠打包方案本身是很難解決這個問題的。但是預校驗問題也不是沒有辦法解決的,解決的思路是當這些類已經被編譯完成之後,在字節碼的層麵去注入一些來自於其他dex的類。
620c0cd7d5c65c29d23c7f08a2d13d9dd948cc45
幸運的是Android的gradle插件也提供了這樣的一些接口,叫做Transform的API。這個API會提供一個調用的時機,當代碼文件被編譯成JAR但是還沒有被打成dex的時候,提供了在這個時期做一些事情的接口。正是利用這個接口,當拿到編譯完成的字節碼文件之後,可以對其進行字節碼的注入,進行所謂的插樁,插入一些來自於其他dex文件的類,這樣當App再被安裝並執行dex opt過程的時候就不會再被打上預校驗的標簽,同時能夠成功加載補丁了,這樣的方案也是非常巧妙並且有效的。

1.5 代碼插樁
但是為什麼慢慢地大家開始覺得Qxxx方案並不好呢?其原因就在於插樁並不是一個非常好的方式,它所帶來的開銷是非常大的。在dex opt的過程中會執行一個驗證的過程,再執行一個優化的過程,最後將dex文件轉成odex文件。因為進行了插樁,所有的類都沒有被打上預校驗的標簽,所以驗證和優化這兩個過程會被放在真正類加載的時候去執行,如果一兩個類在運行的時候進行加載和優化對於App的性能的影響不大,但是現在的App越來越複雜,當有成千上萬的類需要在運行時進行加載和優化的時候,所帶來的開銷就是非常可觀的了。
ef32b7ebaf3dd54a08240a65b94cfc823c1aad9b
曾經有某公司的同學進行過測試,在插樁和不插樁的情況下去比較加載700個類和啟動App的情況。實驗結論是:在插樁的情況下,700個類的加載時間需要600多毫秒,而在不插樁的情況下隻需要80多毫秒,兩者相差了近8倍;在啟動App的層麵,插樁也會明顯比不插樁的情況慢了很多。所以可以說插樁所帶來的性能開銷是非常大的,甚至可以說使用這種方法進行熱修複是一種得不償失的選擇,雖然實現了熱修複的方案,但是因此丟失了程序良好的性能,也正是因為這個原因,Qxxx方案逐漸被生產環境拋棄了。

二、Instant Run方案解析

從嚴格意義上來講,Instant Run其實並不算一個熱修複方案,它隻是一個優化開發效率的機製。在傳統的開發模式中,當在開發的過程中對代碼進行了一些改動就會進行全量的構建,然後將一個完整的App部署到測試機上,之後進行應用重啟,然後就可以看到代碼的變化與運行效果的變化。
f5ce040448c839f7ff095b03bfbd200d4789473a
而隨著App越來越複雜,全量構建的過程本身會變得非常耗時,尤其是在需要頻繁地進行代碼改動觀察效果的時候,這就會嚴重地影響開發效率。基於上述的現狀,Android Studio在2.0版本的時候就發布了Instant Run新特性。Instant Run新特性的原理就是當進行代碼改動之後,會進行增量構建,也就是僅僅構建這部分改變的代碼,並將這部分代碼以補丁的形式增量地部署到設備上,然後進行代碼的熱替換,從而觀察到代碼替換所帶來的效果。其實從某種意義上講,Instant Run和熱修複在本質上是一樣的。

2.1 Instant Run打包邏輯
2d6c801f06efcd6b5842b57c99075c2e195c6a32
在接入Instant Run之後,與傳統方式相比,在進行打包的時候會存在以下四個不同點:
  1. manifest注入;大家都知道一個Android工程的所有組件都會注冊到manifest文件下,在這部分中,Instant Run會生成一個自己的application,然後將這個application注冊到manifest配置文件裏麵去,也就是說當整個App運行起來的時候,首先執行的就是application這個類,也就是運行的是Instant Run本身的框架,它可以去做一係列準備工作,當這些工作完成之後再去運行業務代碼。
  2. Instant Run代碼放入主dex;manifest注入之後,會將Instant Run的代碼放入到Android虛擬機第一個加載的dex文件中,包括classes.dex和classes2.dex,這兩個dex文件存放的都是Instant Run本身框架的代碼,而沒有任何業務層的代碼。正是因為以上的原因,當整個App運行起來的時候首先執行的都是Instant Run的代碼。
  3. 工程代碼插樁——IncretmentalChange;這個插裝裏麵會涉及到具體的IncretmentalChange類。
  4. 工程代碼放入instantrun.zip;這裏的邏輯是當整個App運行起來之後才回去解壓這個包裏麵的具體工程代碼,運行整個業務邏輯。
f29a606ea2d3762966f57380e783b702de6bf743
在App剛開始啟動的時候,Instant Run會做以下三件事情:
  1. 當bootstrap application啟動之後會首先加載classes.dex和classes2.dex這兩個主dex文件,當這兩個主dex文件啟動之後,就會啟動AppServer服務。這裏可以將AppServer理解為一個服務器,它會與IDE也就是Android Studio建立連接。當連接建立之後,後續在開發的過程中的代碼改動所形成的補丁包都會通過這個連接下發到App上,並且通過AppServer接收,再通過相應的處理使得補丁生效。
  2. 當完成了第一個步驟之後,會用本身的ClassLoader去加載instantrun.zip包裏麵真正的工程代碼。
  3. 最後一步,將宿主application替換成真實的realApplication,然後真正地運行自定義application裏麵的邏輯,達到隱藏自身的效果。

2.2 Instant Run熱插拔、溫插拔和冷插拔簡介
當App啟動之後會啟動一個AppServer服務器的連接,當它加載到patch之後會去判斷patch是否能夠進行熱插拔、溫插拔和冷插拔,然後再去做各種方式所對應的事情。
17dcb82633d58e88a330b1fb6b5dbcf8da7dc172
這裏簡單介紹一下在Instant Run裏熱插拔、溫插拔和冷插拔這三個修複方式的概念:
  • 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方法,從而實現了熱修複。
a74316341ab3dc96daf6f9ece1e578bd5a44bbb6

2.4 Instant Run溫插拔(WarmSwap)原理解析
對於溫插拔而言,需要首先簡單介紹一下資源修複的邏輯。其實對於Android框架比較熟悉的同學都清楚,在每一個activity裏麵都會有一個叫做mResource的變量,這個mResource變量指向一個Resource對象。在Resource對象中會存在一個指向AssetManager的mAsset變量,而AssetManager類才是真正去管理和維護所有對於資源的訪問的具體類。AssetManager類裏麵會有兩個具體成員,一個是framework-res.apk,其是係統自帶的資源,另一個則是App本身的資源,而所有對於資源的訪問最終都會走到AssetManager類中。正是因為這樣的機製,Instant Run就是通過替換AssetManager的方式達到資源修複的效果。
883868c3916f47fa56e3d0439e8ad869568bd092
具體來說,當資源發生改變需要進行修複的時候,IDE會發布一個資源的補丁發到終端之上,之後Instant Run會新建一個AssetManager。新建的AssetManager裏麵AppResource的指針就會指向資源的補丁,當指向這個之後再去遍曆所有的activity,將其mAssert指針指向新建的AssertManager。這樣之後所有的activity指向的都是重現建立的AssertManager,此時隻需要去重啟activity,那麼所有的資源訪問就會來自新的資源包裏麵的資源,這樣也就達到了資源修複,也就是溫插拔的效果。
676f394512f28e32a463cebf15df9e69b15f97ea
需要注意的是溫插拔方案隻適用於開發階段,這是因為這種方案在補丁下發的時候會下發一個完整的全量資源包,如果將這種方案應用於線上就會產生比較大的開銷。因為整個App裏麵大部分都屬於資源,如果因為僅僅修複其中的一兩個資源就去下發一個完整的資源包,就會造成較大的開銷。綜上所述,溫插拔方案並不適用於整體線上環境,而隻是一個開發階段的優化手段。

2.5 Instant Run冷插拔(ColdSwap)原理解析
之前提到所有的用戶代碼都被寫到instantrun.zip包裏麵,當代碼結構本身發生了變化之後,可以在把對應的代碼補丁下發到App之上的時候,將對應的patch寫入對應的Instant Run的路徑底下,再重新進行dex opt的過程,之後框架就可以加載對應的類了。
098f39f86cd03af08c7bc1ddbb221625e37b6f01
instantrun.zip裏麵的類都是在運行起來之後才去加載的,這部分的加載是可控的,所以可以進行簡單的dex文件替換,之後再去做修複。之所以dex文件會被切成很多片,是因為如果隻修改了某個類,隻發單一的class補丁放到裏麵同樣會遇到之前所提到的預校驗的問題,所以必須要去進行一次dex opt的過程,才可以繞開預校驗的問題。

2.6 Instant Run方案總結
3609865fc0e648bc8ad354718c90cb055b36cf08
  • 首先,熱插拔的優勢在於其不需要重啟,隻需要代碼補丁被下發到端上之後就可以實時地看到修複效果。但是熱插拔的劣勢也很明顯,因為使用了插樁,所以其性能的開銷會非常大。
  • 其次,對於溫插拔而言,它的優勢是可以實現資源修複,但是其劣勢就是這種方案會下發全量資源包,開銷也是非常大的。
  • 最後,對於冷插拔而言,它支持完整類的替換,但是也存在分包的限製,必須要去做切片,當修改了某一個類之後需要把這個類所有所屬的dex類都打成一個新的dex然後下發到端才可以。

三、Sophix方案解析

Sophix是阿裏巴巴剛剛推出的一款無侵入的熱修複方案,本次分享中就為大家揭曉Sophix的神秘麵紗,看看它到底是怎樣實現的。

3.1 Sophix及時修複(Andfix)原理解析

Sophix也是支持及時修複的,在這一點上與Instant Run一樣,對於方法體邏輯的修改可以在App不重啟的情況下進行。Sophix的及時修複方案其實早在阿裏曾經開源的Andfix方案裏麵就已經實現了。

大家都知道所有的類被加載之後,其方法都會被放在方法區,這是Java層麵的概念,其實這些方法區在native層也就是C層麵都會有各自對應的結構體來描述對應的方法以及執行的邏輯。如果某一個類的方法出現了Bug,那麼可以去新建一個類,把修複後的方法放到這個類裏麵,同時把原來那個類的方法的指針指向新方法的方法體就可以實現方法體的替換,從而實現熱修複的效果。
3afab894014229ac128a7c7369fef05f1ca0bfd2
Andfix方案很早就已經開源了,大家如果感興趣可以去GitHub上拿到源碼進行更進一步的分析。總結而言,就是會通過一個工具對於新的和老的兩個apk進行diff操作,當diff完成之後就可以找到某一個有被修改了的方法的類,當修改之後就會生成一個新的patch的dex文件,這個文件裏麵就會有被修改的方法。首先這個類可能會被改名,但是裏麵會存在一個同名方法,這個方法的注解裏麵會標明其所替換的方法以及被替換方法所屬的類,當這個patch被推送到App端上之後,對應的Andfix的patchManager就會去加載補丁包,首先進行校驗,之後加載補丁裏麵的類,並通過類中的注解去找到它所要替換的類,當兩個類都找到之後再將對應的方法找出來,再在native層麵對於具體方法的邏輯指針進行替換,從而使得指向原有方法的指針指向新的方法,從而達到及時修複的效果。

雖然Andfix及時修複方案看上去很美好,也很漂亮,既能夠實現及時修複又沒有使用插樁付出性能上的代價,但是這個方案也存在很大的限製。之前提到了任何虛擬機在native層都會有對應的結構體來描述方法,而在不同的虛擬機上,描述方法的結構體都是不一樣的,所以需要針對不同的Android虛擬機版本去做不同的適配來匹配不同的結構體,這樣一來兼容性的操作就會非常多。大家都很清楚Android端各個廠商都會定義自己的虛擬機,這時候就無法知道方法所對應的結構體內部是什麼樣的,也就無法實現方法的替換了,所以Andfix方案在現實的環境中存在很大的限製,兼容性會受到非常大的挑戰。
4751f7572a3f2087ae25a9d1036928dd559325db
而在Sophix裏麵卻非常巧妙地解決了上述問題。Sophix方案中提出了一個思想就是不需要去關心方法的結構體內部具體是什麼樣的,隻需要進行一次整體的替換就可以了。原來需要一個成員一個成員地進行遍曆替換,現在是整體替換,但是本質是沒有區別的。同時也不關心結構體內部要做哪些操作以及每個成員變量所代表的含義,隻需要進行整體替換就可以了。這樣做有兩個好處,一個是比較簡單,另一個就是不需要了解虛擬機每個方法體的內部結構,所以理論上對於所有的虛擬機版本都是適用的。

但是,雖然可以使用整體複製的方式去做一次性的結構體替換,但是前提是必須要知道方法結構體的尺寸大小,隻有在知道這些之後才能進行替換,這也就是第二個難題,因為不同的虛擬機版本的結構體大小也不同,那麼如何去知道結構體大小呢?對於這一點Sophix方案的解決方法也非常巧妙。
fbccc1444a7205d43892f40c16e79f3e55b9628c
大家都知道當一個類被加載的時候,其方法都會被放在方法區,而且同一個類的方法會被緊密地排列在一起,而我們可以拿到兩個方法的地址起始值,而這兩者之間的差值就是第一個方法結構體的大小。正是基於這一點,隻要去構造一個隻有兩個靜態方法的類,那麼就可以通過獲取這兩個方法的起始地址相減拿到方法對應結構體的大小,從而進行整體的替換。

總之,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文件的不同卻決定了它們需要采用不同熱修複方案的原因。
f427e489effc9e026b256993f3a9f3ba382a325d
ART本身支持多個dex加載,所以在程序啟動之前ART已經把多個dex文件都加載進來了,在運行後所有的類都已經被放在ClassLoader裏麵了,而不需要再去做加載工作。而Dalvik使用的multi-dex方案實際上在程序運行之前隻加載了classes.dex文件,而剩下的其他的dex文件都是在程序啟動之後application運行的時候再去加載的,所以可以看作在程序運行時進行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下的冷啟動修複就是這樣實現的。
02fd6fac3465020d5c4ce0797f7eced79a1f14bc
這裏進行了一次整體的替換而不再隻是某一個dex文件的插入,並且重新執行了dex opt的過程,所以這裏不會出現預校驗的問題。同時以前和補丁包在同一個dex文件的這些類因為補丁包被打到了新的dex文件中,它們的預檢驗標簽會被去除掉,但是由於這些類的數量很少,所以對於性能的影響也是比較小的,做到了最大程度地降低了熱修複所帶來的性能開銷。而因為loadDex的過程是非常耗時的,所以在真正實現的時候會做通過異步的方式另外開辟一個線程去做dex與odex的轉換。當App重新啟動,新的odex全部生成之後才會去做dexElement數組的替換,這就最大程度地保證了App運行的穩定性。因為熱修複方案的目標是幫助修複問題,所以不能再帶來其他的穩定性問題,所以Sophix方案在這裏花費了很大精力進行優化。

Dalvik的冷啟動修複
下表中除了Sophix還列舉了另外兩個方案進行對比來看。
7ca769081951973c68d7c0b9add76fd2474df005
這裏Qxxx方案原理是dexElement注入,特點是實現比較簡單,但是由於基於插樁實現,所以本身類加載的開銷比較大,所以不適用於生產環境。Txxx方案和Sophix都是全量地替換dex,同時Txxx也不使用插樁的方式,最大程度地保障了性能,最具有的特點的就是自研了dex合並算法,最大限度地減少了代碼體積,而這一點是它的優勢也是它的劣勢,需要比較細粒度地做dex合並,從而可以把多個dex合並成為一個dex再去加載。但是這個自研的dex合並算法比較複雜,然而帶來的收益卻並不大,雖然減少了代碼體積,但是一個App的體積裏麵的大部分並不是代碼本身而是資源,所以通過合並算法減少的代碼體積隻是很少的一部分,所以性價比很低。而在Sophix裏麵,雖然也做了全量的替換,但是相對而言比較簡單,以類為粒度合並dex,雖然沒有減少dex文件的體積,但是合並的方法更加簡單明了。

在冷啟動修複下,Sophix方案有一個很簡單的思想就是當發現某些類存在Bug下發新的補丁之後,如果把原有的存在Bug的類從原來所屬的dex摳出來再去執行加載的時候,因為原有的dex文件不再有這些類了,此時就會去從patch.dex文件中加載到它,這樣就可以實現熱修複的效果,並且這樣並不會有預檢驗的問題,從而最大程度地保證了程序性能。所以這個方案中最困難的一點就是如何把以前這些有Bug的類從dex中摳出來,這個問題可能會非常複雜,因為每個類的大小都不一樣,如何將其從連續排列的內存空間中取出來然後再去做移位操作,這樣想起來很複雜。而實際上Sophix使用了一個很巧妙的方式實現這樣的事情。
a641e2f1acaeea4042f48d877bee971699d4d7b9
在dex文件中其實會有一個叫做dexHeader的結構,這就是dex文件的文件頭,在這裏麵有兩個成員與實現緊密相關:一個叫做classDefsSize,另一個叫做classDefsOff。ClassDef也就是所有的類定義,這個dex文件裏麵所有涉及到的類都會被注冊到classDef的結構體裏麵,classDefsOff指的就是classDef結構的偏移量,所以通過classDefsOff就可以找到對應的dex文件裏麵的classDef,從而找到這個dex文件中到底有哪些類,虛擬機也是同樣的原理,通過classDefsOff偏移量找到classDef再遍曆並加載相應的類。而想要實現熱修複隻需要在classDef裏麵將需要被替換的類抹掉就可以了。所以在Dalvik下麵的邏輯就是通過DexHeader找到classDefsOff,再在classDefs裏麵找到需要修改的類,把這些類抹掉再去修改classDefsSize,這樣當虛擬機再去加載dex文件的時候就會認為被修複的類不包含在dex文件裏麵,盡管實際上這些類的實現還是在dex文件裏麵的。
19592b2603370274dd3f35caeb6541f89922718b

3.3 Sophix資源修複原理解析
之前在Instant Run裏麵也提到了資源修複,因為資源修複下發的是全量的資源包,所以並不適合在線上的環境中應用。想要在線上環境做資源修複肯定會使用差量的資源包,下發更新過的資源,而Sophix也是這樣做的。
fe3424550b7ee8aef3d2d3510bd417c681a1834e
之前提到所有資源的訪問都是被AssetManager類所代理的,AssetManager裏麵會有一個AssetPath數組,其中會有兩個成員,一個是指向係統框架資源,一個應用的指向資源。Sophix資源修複的思路就是在AssetPath數組裏麵多加一個資源,這樣當在前兩個資源路徑中都找不到時候就可以從新的資源路徑中尋找,可以通過這樣的方式去修複資源替換的問題。當資源修改之後會下發一個資源修複的補丁包,同時把補丁包集成到端上之後會在AssetManager裏麵通過addAssetPath方法添加一個新的成員,進而實現資源修複。

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

四、熱修複方案的總結和對比
c39778340cadad4f02cc446d59a332cd1602e628
最後來總結和對比各種修複方案的原理和特點:
  • Qxxx方案原理比較簡單,通過代碼插樁繞開預校驗問題,此外通過dexElement插入的方式使得帶補丁的dex文件優先加載。其優點在於實現比較簡單,可以修複大部分類層麵的問題。但是同時其問題也是比較突出的,第一點是不支持實時生效,第二點就是全量插樁的方式侵入性非常強,同時性能損耗也是非常大的。
  • Instant Run方案原理同樣用到了代碼插樁,而且其插樁比Qxxx方案更加複雜,它還會用到宿主的application做很多準備工作,這之後才會去執行業務代碼,最後它還會去通過AssetManager重建做資源修複工作。優點是它能同時支持方法更新、類更新和資源更新,並且在方法更新的過程中還可以做到及時修複,不需要重啟App。其問題在於還是使用了全量插樁,所以侵入性很強,同時對於性能的損耗也很大,由於在進行修複時需要下發全量資源包,所以開銷非常大,同時也不適合在實際的生產環境中使用。
  • Andfix方案的原理是native方法的替換,這個方法很巧妙,並且實現比較簡單,而且可以做到及時生效。但是它不支持類結構的改變,同時因為不同版本虛擬機的方法體結構不同,無法實現兼容性的處理,所以這種方案的兼容性也比較差。
  • Sophix方案使用了很多很巧妙的原理實現,首先它還是使用native方法替換,這種方法會比Andfix更加巧妙,它不需要知道方法結構體具體的成員變量,而直接使用整體的替換,隻需要知道方法結構體的大小即可。它使用了很巧妙的方式,通過兩個緊密排列的方法的地址差完成了方法替換即時生效的功能。Sophix使用全量dex替換去完成冷啟動修複場景,而在資源修複的時候使用了差量資源包注入的方式,最大限度地降低了網絡的開銷,隻需要很輕量地把差量資源包下發就可以了。其優點在於同時支持方法更新、類更新和資源更新,而且包括native方法的替換以及資源包的注入等很多實現非常巧妙和優雅,也非常輕量,並且屬於非侵入式的修複。

最後更新:2017-11-03 14:03:48

  上一篇:go  數據智能助力智慧航空:阿裏雲雙十一特別訪談
  下一篇:go  Day2&Day3@JavaOne2017