干貨滿滿,Android熱修復(fù)方案介紹(轉(zhuǎn)載)

原文鏈接:https://developer.aliyun.com/article/231111

場景研讀 2017-11-03 10747瀏覽量

簡介: 在云棲社區(qū)技術(shù)直播中,阿里云客戶端工程師李亞洲(畢言)從技術(shù)原理層面解析和比較了業(yè)界幾大熱修復(fù)方案,揭開了Qxxx方案、Instant Run以及阿里Sophix等熱修復(fù)方案的神秘面紗,幫助大家更加深刻地理解了代碼插樁、全量dex替換、資源修復(fù)等常見場景解決方案,本文干貨滿滿,精彩不容錯(cuò)過。

摘要:在云棲社區(qū)技術(shù)直播中,阿里云客戶端工程師李亞洲(畢言)從技術(shù)原理層面解析和比較了業(yè)界幾大熱修復(fù)方案,揭開了Qxxx方案、Instant Run以及阿里Sophix等熱修復(fù)方案的神秘面紗,幫助大家更加深刻地理解了代碼插樁、全量dex替換、資源修復(fù)等常見場景解決方案,本文干貨滿滿,精彩不容錯(cuò)過。

以下內(nèi)容根據(jù)演講視頻以及PPT整理而成。

視頻分享鏈接,點(diǎn)擊這里!

在傳統(tǒng)的修復(fù)模式下,如果線上的App出現(xiàn)Bug之后進(jìn)行修復(fù)所需要的時(shí)間成本非常高,這是因?yàn)橥枰l(fā)布一個(gè)新的版本,然后將其發(fā)布到對(duì)應(yīng)的應(yīng)用商城中,然后通知用戶下載和更新自己的App。但是尤其在Android這樣沒有統(tǒng)一應(yīng)用市場的環(huán)境下,修復(fù)周期可能需要以周來計(jì)數(shù)。而隨著App的業(yè)務(wù)越來復(fù)雜、代碼量越來越大,出現(xiàn)Bug的概率也會(huì)越來越高,如果繼續(xù)按照傳統(tǒng)的修復(fù)模式就很難滿足業(yè)務(wù)發(fā)展的需求。正是因?yàn)檫@樣的現(xiàn)狀,很多Android端開發(fā)的同學(xué)就開始思考是否有一種在不發(fā)版的前提下修復(fù)線上Bug的技術(shù),也就是所謂的熱修復(fù)技術(shù)。通過這幾年的發(fā)展,熱修復(fù)技術(shù)也得到了很大的發(fā)展,很多公司也具有了比較成熟的熱修復(fù)方案,同時(shí)這些熱修復(fù)方案也大規(guī)模地在生產(chǎn)環(huán)境中進(jìn)行了實(shí)踐。本次會(huì)選取幾個(gè)比較具有代表性的熱修復(fù)方案與大家分享。

在本次的分享中主要會(huì)講到三種技術(shù)方案:Qxxx(化名)方案、Instant Run和Sophix。Qxxx屬于比較早期的修復(fù)方案,而現(xiàn)在由于種種原因可能不會(huì)成為大家在進(jìn)行熱修復(fù)時(shí)的首選方案,但是其技術(shù)原理給了我們很大啟發(fā),所以在本次的技術(shù)分享中將Qxxx方案放在第一個(gè)進(jìn)行介紹。第二種方案叫做Instant Run,這是所有從事Android開發(fā)的同學(xué)都比較熟悉的一種方案,嚴(yán)格意義上來講Instant Run其實(shí)不算是一個(gè)熱修復(fù)的方案,它僅僅是作為Android Studio來提高開發(fā)效率的一個(gè)功能而已,但是其背后的技術(shù)原理卻和很多修復(fù)方案具有很多相通的地方。第三種方案叫做Sophix,它是阿里巴巴剛剛發(fā)布的一種修復(fù)方案,它代表著新一代的熱修復(fù)方案,Sophix的功能更加強(qiáng)大而且能夠覆蓋的場景也非常廣,所以也是一個(gè)比較優(yōu)秀的方案。

一、Qxxx方案解析

1.1 Qxxx方案原理介紹

[圖片上傳失敗...(image-ccc58d-1596530601128)]

可能大家對(duì)于Qxxx方案比較了解,它也是來自于業(yè)界一家非常優(yōu)秀的公司所提供的方案。首先分享Qxxx方案比較基礎(chǔ)的特性,它是基于Android dex分包方案,其次它最關(guān)鍵的技術(shù)點(diǎn)在于利用字節(jié)碼插樁的方式繞開了預(yù)校驗(yàn)問題,這也是Qxxx方案最為核心的一點(diǎn)。Qxxx只支持App重啟之后才能修復(fù),也就是App在運(yùn)行的時(shí)候加載到了補(bǔ)丁包也不能及時(shí)修復(fù),需要App重新啟動(dòng)的時(shí)候才會(huì)修復(fù),這是因?yàn)镼xxx方案是基于類加載區(qū)需要重新加載補(bǔ)丁類才能實(shí)現(xiàn)的,所以必須進(jìn)行重啟才能修復(fù)。此外,Qxxx方案只支持到類結(jié)構(gòu)本身代碼層面的修復(fù),不支持資源的修復(fù)。

1.2 Android端的類加載原理
接下來為大家簡單介紹Android端類的加載原理。其實(shí)Android的類加載和Java的類加載比較類似,都是通過ClassLoader類加載器進(jìn)行加載,唯一的區(qū)別就是Android類加載器加載的是dex文件,所以在Android中加載器的基類叫做BaseDexClassLoader,這個(gè)類之下會(huì)有兩個(gè)子類,一個(gè)叫做PathClassLoader,它負(fù)責(zé)加載Android的SDK,當(dāng)代碼中引用到Android框架的本身類的時(shí)候都是通過PathClassLoader進(jìn)行加載的;而另外一個(gè)子類叫做DexClassLoader,這個(gè)類就是用于加載業(yè)務(wù)層面代碼的加載器。

[圖片上傳失敗...(image-6daa86-1596530601128)]

早期在Android端只能加載一個(gè)dex文件,后來隨著代碼越來越多,一個(gè)dex文件已經(jīng)無法存放所有代碼了,所以需要加載多個(gè)dex文件,這無論是在什么虛擬機(jī)內(nèi)都可以通過相同的手法解決。而解決的關(guān)鍵就是無論有幾個(gè)dex文件,所有的dex文件最后最后被加入到內(nèi)存中都會(huì)形成一個(gè)數(shù)組叫做dexElement。而DexClassLoader的原理就是當(dāng)系統(tǒng)需要加載某一個(gè)類的時(shí)候,DexClassLoader就會(huì)去遍歷dexElement數(shù)組,當(dāng)它遍歷到某一個(gè)元素的時(shí)候,在這個(gè)元素所對(duì)應(yīng)的文件找到目標(biāo)類的時(shí)候就會(huì)停止遍歷并及時(shí)返回。所以當(dāng)在不同的dex文件里面出現(xiàn)兩個(gè)一樣的類的時(shí)候,哪一個(gè)被先遍歷到哪個(gè)就會(huì)被先加載。正是基于這樣的原理,Qxxx方案的研發(fā)同學(xué)就想到如果編寫的代碼存在Bug,就可以發(fā)布一個(gè)補(bǔ)丁包,而這個(gè)補(bǔ)丁包也是dex文件,并且使這個(gè)dex文件能夠首先被DexClassLoader加載到,就能夠達(dá)到修復(fù)的效果。所以就是需要通過一定的手段將補(bǔ)丁包的dex文件放在dexElement數(shù)組中的第一位,那么就可以首先加載補(bǔ)丁中的類,進(jìn)而達(dá)到修復(fù)的效果。

1.3 類預(yù)校驗(yàn)問題
而實(shí)際情況中,做這樣的方案是不行的。直接進(jìn)行替換就會(huì)出現(xiàn)預(yù)校驗(yàn)的問題。

[圖片上傳失敗...(image-8c19d-1596530601128)]

而預(yù)校驗(yàn)問題出現(xiàn)的原因在于當(dāng)App被安裝到設(shè)備上之后,會(huì)存在一個(gè)叫做dex opt的過程,也就是將dex文件進(jìn)行優(yōu)化,在優(yōu)化之后dex文件將會(huì)變成odex文件,而在優(yōu)化過程中就會(huì)有一個(gè)預(yù)校驗(yàn)的問題。當(dāng)某一個(gè)類所有的構(gòu)造方法、私有方法以及重載方法所引用的其他類和這個(gè)類本身都來自于同一個(gè)dex文件的時(shí)候,這個(gè)類就會(huì)被打上class_ispreverified標(biāo)簽。所以如果加載的類來自于補(bǔ)丁文件,而補(bǔ)丁文件和之前的文件必然不屬于同一個(gè)dex,而本身的那個(gè)類已經(jīng)被打上了class_ispreverified標(biāo)簽,但是在運(yùn)行時(shí)又引用了其他dex的類,這樣就必然會(huì)出現(xiàn)錯(cuò)誤。所以Qxxx方案所需要解決的關(guān)鍵問題就是預(yù)校驗(yàn)。

[圖片上傳失敗...(image-64384b-1596530601128)]

1.4 字節(jié)碼注入
針對(duì)預(yù)校驗(yàn)問題,有同學(xué)可能會(huì)認(rèn)為只需要在寫代碼的時(shí)候引用一些類就可以了,但是這在實(shí)際情況下卻是非常困難的,因?yàn)楸旧鞟ndroid在打包代碼的時(shí)候就會(huì)盡可能地將相互依賴的類打包在同一個(gè)dex里面,所以依靠打包方案本身是很難解決這個(gè)問題的。但是預(yù)校驗(yàn)問題也不是沒有辦法解決的,解決的思路是當(dāng)這些類已經(jīng)被編譯完成之后,在字節(jié)碼的層面去注入一些來自于其他dex的類。

[圖片上傳失敗...(image-c3032b-1596530601128)]

幸運(yùn)的是Android的gradle插件也提供了這樣的一些接口,叫做Transform的API。這個(gè)API會(huì)提供一個(gè)調(diào)用的時(shí)機(jī),當(dāng)代碼文件被編譯成JAR但是還沒有被打成dex的時(shí)候,提供了在這個(gè)時(shí)期做一些事情的接口。正是利用這個(gè)接口,當(dāng)拿到編譯完成的字節(jié)碼文件之后,可以對(duì)其進(jìn)行字節(jié)碼的注入,進(jìn)行所謂的插樁,插入一些來自于其他dex文件的類,這樣當(dāng)App再被安裝并執(zhí)行dex opt過程的時(shí)候就不會(huì)再被打上預(yù)校驗(yàn)的標(biāo)簽,同時(shí)能夠成功加載補(bǔ)丁了,這樣的方案也是非常巧妙并且有效的。

1.5 代碼插樁
但是為什么慢慢地大家開始覺得Qxxx方案并不好呢?其原因就在于插樁并不是一個(gè)非常好的方式,它所帶來的開銷是非常大的。在dex opt的過程中會(huì)執(zhí)行一個(gè)驗(yàn)證的過程,再執(zhí)行一個(gè)優(yōu)化的過程,最后將dex文件轉(zhuǎn)成odex文件。因?yàn)檫M(jìn)行了插樁,所有的類都沒有被打上預(yù)校驗(yàn)的標(biāo)簽,所以驗(yàn)證和優(yōu)化這兩個(gè)過程會(huì)被放在真正類加載的時(shí)候去執(zhí)行,如果一兩個(gè)類在運(yùn)行的時(shí)候進(jìn)行加載和優(yōu)化對(duì)于App的性能的影響不大,但是現(xiàn)在的App越來越復(fù)雜,當(dāng)有成千上萬的類需要在運(yùn)行時(shí)進(jìn)行加載和優(yōu)化的時(shí)候,所帶來的開銷就是非??捎^的了。

[圖片上傳失敗...(image-a95e1-1596530601128)]

曾經(jīng)有某公司的同學(xué)進(jìn)行過測試,在插樁和不插樁的情況下去比較加載700個(gè)類和啟動(dòng)App的情況。實(shí)驗(yàn)結(jié)論是:在插樁的情況下,700個(gè)類的加載時(shí)間需要600多毫秒,而在不插樁的情況下只需要80多毫秒,兩者相差了近8倍;在啟動(dòng)App的層面,插樁也會(huì)明顯比不插樁的情況慢了很多。所以可以說插樁所帶來的性能開銷是非常大的,甚至可以說使用這種方法進(jìn)行熱修復(fù)是一種得不償失的選擇,雖然實(shí)現(xiàn)了熱修復(fù)的方案,但是因此丟失了程序良好的性能,也正是因?yàn)檫@個(gè)原因,Qxxx方案逐漸被生產(chǎn)環(huán)境拋棄了。

二、Instant Run方案解析

從嚴(yán)格意義上來講,Instant Run其實(shí)并不算一個(gè)熱修復(fù)方案,它只是一個(gè)優(yōu)化開發(fā)效率的機(jī)制。在傳統(tǒng)的開發(fā)模式中,當(dāng)在開發(fā)的過程中對(duì)代碼進(jìn)行了一些改動(dòng)就會(huì)進(jìn)行全量的構(gòu)建,然后將一個(gè)完整的App部署到測試機(jī)上,之后進(jìn)行應(yīng)用重啟,然后就可以看到代碼的變化與運(yùn)行效果的變化。

[圖片上傳失敗...(image-70055e-1596530601128)]

而隨著App越來越復(fù)雜,全量構(gòu)建的過程本身會(huì)變得非常耗時(shí),尤其是在需要頻繁地進(jìn)行代碼改動(dòng)觀察效果的時(shí)候,這就會(huì)嚴(yán)重地影響開發(fā)效率?;谏鲜龅默F(xiàn)狀,Android Studio在2.0版本的時(shí)候就發(fā)布了Instant Run新特性。Instant Run新特性的原理就是當(dāng)進(jìn)行代碼改動(dòng)之后,會(huì)進(jìn)行增量構(gòu)建,也就是僅僅構(gòu)建這部分改變的代碼,并將這部分代碼以補(bǔ)丁的形式增量地部署到設(shè)備上,然后進(jìn)行代碼的熱替換,從而觀察到代碼替換所帶來的效果。其實(shí)從某種意義上講,Instant Run和熱修復(fù)在本質(zhì)上是一樣的。

2.1 Instant Run打包邏輯

[圖片上傳失敗...(image-ce6bb1-1596530601128)]

在接入Instant Run之后,與傳統(tǒng)方式相比,在進(jìn)行打包的時(shí)候會(huì)存在以下四個(gè)不同點(diǎn):

  1. manifest注入;大家都知道一個(gè)Android工程的所有組件都會(huì)注冊(cè)到manifest文件下,在這部分中,Instant Run會(huì)生成一個(gè)自己的application,然后將這個(gè)application注冊(cè)到manifest配置文件里面去,也就是說當(dāng)整個(gè)App運(yùn)行起來的時(shí)候,首先執(zhí)行的就是application這個(gè)類,也就是運(yùn)行的是Instant Run本身的框架,它可以去做一系列準(zhǔn)備工作,當(dāng)這些工作完成之后再去運(yùn)行業(yè)務(wù)代碼。
  2. Instant Run代碼放入主dex;manifest注入之后,會(huì)將Instant Run的代碼放入到Android虛擬機(jī)第一個(gè)加載的dex文件中,包括classes.dex和classes2.dex,這兩個(gè)dex文件存放的都是Instant Run本身框架的代碼,而沒有任何業(yè)務(wù)層的代碼。正是因?yàn)橐陨系脑?,?dāng)整個(gè)App運(yùn)行起來的時(shí)候首先執(zhí)行的都是Instant Run的代碼。
  3. 工程代碼插樁——IncretmentalChange;這個(gè)插裝里面會(huì)涉及到具體的IncretmentalChange類。
  4. 工程代碼放入instantrun.zip;這里的邏輯是當(dāng)整個(gè)App運(yùn)行起來之后才回去解壓這個(gè)包里面的具體工程代碼,運(yùn)行整個(gè)業(yè)務(wù)邏輯。

[圖片上傳失敗...(image-9aeb9b-1596530601128)]

在App剛開始啟動(dòng)的時(shí)候,Instant Run會(huì)做以下三件事情:

  1. 當(dāng)bootstrap application啟動(dòng)之后會(huì)首先加載classes.dex和classes2.dex這兩個(gè)主dex文件,當(dāng)這兩個(gè)主dex文件啟動(dòng)之后,就會(huì)啟動(dòng)AppServer服務(wù)。這里可以將AppServer理解為一個(gè)服務(wù)器,它會(huì)與IDE也就是Android Studio建立連接。當(dāng)連接建立之后,后續(xù)在開發(fā)的過程中的代碼改動(dòng)所形成的補(bǔ)丁包都會(huì)通過這個(gè)連接下發(fā)到App上,并且通過AppServer接收,再通過相應(yīng)的處理使得補(bǔ)丁生效。
  2. 當(dāng)完成了第一個(gè)步驟之后,會(huì)用本身的ClassLoader去加載instantrun.zip包里面真正的工程代碼。
  3. 最后一步,將宿主application替換成真實(shí)的realApplication,然后真正地運(yùn)行自定義application里面的邏輯,達(dá)到隱藏自身的效果。

2.2 Instant Run熱插拔、溫插拔和冷插拔簡介
當(dāng)App啟動(dòng)之后會(huì)啟動(dòng)一個(gè)AppServer服務(wù)器的連接,當(dāng)它加載到patch之后會(huì)去判斷patch是否能夠進(jìn)行熱插拔、溫插拔和冷插拔,然后再去做各種方式所對(duì)應(yīng)的事情。

[圖片上傳失敗...(image-9f3bf1-1596530601128)]

這里簡單介紹一下在Instant Run里熱插拔、溫插拔和冷插拔這三個(gè)修復(fù)方式的概念:

  • HotSwap(熱插拔):修改方法實(shí)現(xiàn)后代碼可以實(shí)時(shí)生效,不需要重啟App也不需要重啟activity,只要加載補(bǔ)丁之后就可以馬上生效。通常情況下,熱插拔只適用于方法體內(nèi)部的邏輯改變。
  • WarmSwap(溫插拔):主要針對(duì)于需要修改或刪除資源的情況。溫插拔不需要重啟App,但是需要重啟當(dāng)前的activity后才能生效。
  • ColdSwap(冷插拔):主要針對(duì)于改變了類的結(jié)構(gòu)、繼承關(guān)系、實(shí)現(xiàn)接口等情況,此時(shí)因?yàn)轭惤Y(jié)構(gòu)本身被改變了,需要重新去加載這個(gè)類,所以需要重啟App之后才能生效。

2.3 Instant Run熱插拔(HotSwap)原理解析
首先IDE下發(fā)patch,加載到補(bǔ)丁之后,在App層Instant Run的框架會(huì)通過AppPatchLoader去找到哪些類需要被修復(fù),當(dāng)找到需要被修復(fù)的類之后再通過反射的手段將類中的$change變量設(shè)置為已經(jīng)修復(fù)后的類。這樣當(dāng)執(zhí)行MainActivity的onClick方法的時(shí)候?qū)嶋H上執(zhí)行到的是MainActivity&override的onClick方法,從而實(shí)現(xiàn)了熱修復(fù)。

[圖片上傳失敗...(image-c4406f-1596530601128)]

2.4 Instant Run溫插拔(WarmSwap)原理解析
對(duì)于溫插拔而言,需要首先簡單介紹一下資源修復(fù)的邏輯。其實(shí)對(duì)于Android框架比較熟悉的同學(xué)都清楚,在每一個(gè)activity里面都會(huì)有一個(gè)叫做mResource的變量,這個(gè)mResource變量指向一個(gè)Resource對(duì)象。在Resource對(duì)象中會(huì)存在一個(gè)指向AssetManager的mAsset變量,而AssetManager類才是真正去管理和維護(hù)所有對(duì)于資源的訪問的具體類。AssetManager類里面會(huì)有兩個(gè)具體成員,一個(gè)是framework-res.apk,其是系統(tǒng)自帶的資源,另一個(gè)則是App本身的資源,而所有對(duì)于資源的訪問最終都會(huì)走到AssetManager類中。正是因?yàn)檫@樣的機(jī)制,Instant Run就是通過替換AssetManager的方式達(dá)到資源修復(fù)的效果。

[圖片上傳失敗...(image-af0940-1596530601127)]

具體來說,當(dāng)資源發(fā)生改變需要進(jìn)行修復(fù)的時(shí)候,IDE會(huì)發(fā)布一個(gè)資源的補(bǔ)丁發(fā)到終端之上,之后Instant Run會(huì)新建一個(gè)AssetManager。新建的AssetManager里面AppResource的指針就會(huì)指向資源的補(bǔ)丁,當(dāng)指向這個(gè)之后再去遍歷所有的activity,將其mAssert指針指向新建的AssertManager。這樣之后所有的activity指向的都是重現(xiàn)建立的AssertManager,此時(shí)只需要去重啟activity,那么所有的資源訪問就會(huì)來自新的資源包里面的資源,這樣也就達(dá)到了資源修復(fù),也就是溫插拔的效果。

[圖片上傳失敗...(image-fd0d2c-1596530601127)]

需要注意的是溫插拔方案只適用于開發(fā)階段,這是因?yàn)檫@種方案在補(bǔ)丁下發(fā)的時(shí)候會(huì)下發(fā)一個(gè)完整的全量資源包,如果將這種方案應(yīng)用于線上就會(huì)產(chǎn)生比較大的開銷。因?yàn)檎麄€(gè)App里面大部分都屬于資源,如果因?yàn)閮H僅修復(fù)其中的一兩個(gè)資源就去下發(fā)一個(gè)完整的資源包,就會(huì)造成較大的開銷。綜上所述,溫插拔方案并不適用于整體線上環(huán)境,而只是一個(gè)開發(fā)階段的優(yōu)化手段。

2.5 Instant Run冷插拔(ColdSwap)原理解析
之前提到所有的用戶代碼都被寫到instantrun.zip包里面,當(dāng)代碼結(jié)構(gòu)本身發(fā)生了變化之后,可以在把對(duì)應(yīng)的代碼補(bǔ)丁下發(fā)到App之上的時(shí)候,將對(duì)應(yīng)的patch寫入對(duì)應(yīng)的Instant Run的路徑底下,再重新進(jìn)行dex opt的過程,之后框架就可以加載對(duì)應(yīng)的類了。

[圖片上傳失敗...(image-1c1b19-1596530601127)]

instantrun.zip里面的類都是在運(yùn)行起來之后才去加載的,這部分的加載是可控的,所以可以進(jìn)行簡單的dex文件替換,之后再去做修復(fù)。之所以dex文件會(huì)被切成很多片,是因?yàn)槿绻恍薷牧四硞€(gè)類,只發(fā)單一的class補(bǔ)丁放到里面同樣會(huì)遇到之前所提到的預(yù)校驗(yàn)的問題,所以必須要去進(jìn)行一次dex opt的過程,才可以繞開預(yù)校驗(yàn)的問題。

2.6 Instant Run方案總結(jié)

[圖片上傳失敗...(image-f42361-1596530601127)]

  • 首先,熱插拔的優(yōu)勢在于其不需要重啟,只需要代碼補(bǔ)丁被下發(fā)到端上之后就可以實(shí)時(shí)地看到修復(fù)效果。但是熱插拔的劣勢也很明顯,因?yàn)槭褂昧瞬鍢?,所以其性能的開銷會(huì)非常大。
  • 其次,對(duì)于溫插拔而言,它的優(yōu)勢是可以實(shí)現(xiàn)資源修復(fù),但是其劣勢就是這種方案會(huì)下發(fā)全量資源包,開銷也是非常大的。
  • 最后,對(duì)于冷插拔而言,它支持完整類的替換,但是也存在分包的限制,必須要去做切片,當(dāng)修改了某一個(gè)類之后需要把這個(gè)類所有所屬的dex類都打成一個(gè)新的dex然后下發(fā)到端才可以。

三、Sophix方案解析

Sophix是阿里巴巴剛剛推出的一款無侵入的熱修復(fù)方案,本次分享中就為大家揭曉Sophix的神秘面紗,看看它到底是怎樣實(shí)現(xiàn)的。
3.1 Sophix及時(shí)修復(fù)(Andfix)原理解析
Sophix也是支持及時(shí)修復(fù)的,在這一點(diǎn)上與Instant Run一樣,對(duì)于方法體邏輯的修改可以在App不重啟的情況下進(jìn)行。Sophix的及時(shí)修復(fù)方案其實(shí)早在阿里曾經(jīng)開源的Andfix方案里面就已經(jīng)實(shí)現(xiàn)了。

大家都知道所有的類被加載之后,其方法都會(huì)被放在方法區(qū),這是Java層面的概念,其實(shí)這些方法區(qū)在native層也就是C層面都會(huì)有各自對(duì)應(yīng)的結(jié)構(gòu)體來描述對(duì)應(yīng)的方法以及執(zhí)行的邏輯。如果某一個(gè)類的方法出現(xiàn)了Bug,那么可以去新建一個(gè)類,把修復(fù)后的方法放到這個(gè)類里面,同時(shí)把原來那個(gè)類的方法的指針指向新方法的方法體就可以實(shí)現(xiàn)方法體的替換,從而實(shí)現(xiàn)熱修復(fù)的效果。

[圖片上傳失敗...(image-1f991-1596530601127)]

Andfix方案很早就已經(jīng)開源了,大家如果感興趣可以去GitHub上拿到源碼進(jìn)行更進(jìn)一步的分析??偨Y(jié)而言,就是會(huì)通過一個(gè)工具對(duì)于新的和老的兩個(gè)apk進(jìn)行diff操作,當(dāng)diff完成之后就可以找到某一個(gè)有被修改了的方法的類,當(dāng)修改之后就會(huì)生成一個(gè)新的patch的dex文件,這個(gè)文件里面就會(huì)有被修改的方法。首先這個(gè)類可能會(huì)被改名,但是里面會(huì)存在一個(gè)同名方法,這個(gè)方法的注解里面會(huì)標(biāo)明其所替換的方法以及被替換方法所屬的類,當(dāng)這個(gè)patch被推送到App端上之后,對(duì)應(yīng)的Andfix的patchManager就會(huì)去加載補(bǔ)丁包,首先進(jìn)行校驗(yàn),之后加載補(bǔ)丁里面的類,并通過類中的注解去找到它所要替換的類,當(dāng)兩個(gè)類都找到之后再將對(duì)應(yīng)的方法找出來,再在native層面對(duì)于具體方法的邏輯指針進(jìn)行替換,從而使得指向原有方法的指針指向新的方法,從而達(dá)到及時(shí)修復(fù)的效果。

雖然Andfix及時(shí)修復(fù)方案看上去很美好,也很漂亮,既能夠?qū)崿F(xiàn)及時(shí)修復(fù)又沒有使用插樁付出性能上的代價(jià),但是這個(gè)方案也存在很大的限制。之前提到了任何虛擬機(jī)在native層都會(huì)有對(duì)應(yīng)的結(jié)構(gòu)體來描述方法,而在不同的虛擬機(jī)上,描述方法的結(jié)構(gòu)體都是不一樣的,所以需要針對(duì)不同的Android虛擬機(jī)版本去做不同的適配來匹配不同的結(jié)構(gòu)體,這樣一來兼容性的操作就會(huì)非常多。大家都很清楚Android端各個(gè)廠商都會(huì)定義自己的虛擬機(jī),這時(shí)候就無法知道方法所對(duì)應(yīng)的結(jié)構(gòu)體內(nèi)部是什么樣的,也就無法實(shí)現(xiàn)方法的替換了,所以Andfix方案在現(xiàn)實(shí)的環(huán)境中存在很大的限制,兼容性會(huì)受到非常大的挑戰(zhàn)。

[圖片上傳失敗...(image-4ab268-1596530601127)]

而在Sophix里面卻非常巧妙地解決了上述問題。Sophix方案中提出了一個(gè)思想就是不需要去關(guān)心方法的結(jié)構(gòu)體內(nèi)部具體是什么樣的,只需要進(jìn)行一次整體的替換就可以了。原來需要一個(gè)成員一個(gè)成員地進(jìn)行遍歷替換,現(xiàn)在是整體替換,但是本質(zhì)是沒有區(qū)別的。同時(shí)也不關(guān)心結(jié)構(gòu)體內(nèi)部要做哪些操作以及每個(gè)成員變量所代表的含義,只需要進(jìn)行整體替換就可以了。這樣做有兩個(gè)好處,一個(gè)是比較簡單,另一個(gè)就是不需要了解虛擬機(jī)每個(gè)方法體的內(nèi)部結(jié)構(gòu),所以理論上對(duì)于所有的虛擬機(jī)版本都是適用的。

但是,雖然可以使用整體復(fù)制的方式去做一次性的結(jié)構(gòu)體替換,但是前提是必須要知道方法結(jié)構(gòu)體的尺寸大小,只有在知道這些之后才能進(jìn)行替換,這也就是第二個(gè)難題,因?yàn)椴煌奶摂M機(jī)版本的結(jié)構(gòu)體大小也不同,那么如何去知道結(jié)構(gòu)體大小呢?對(duì)于這一點(diǎn)Sophix方案的解決方法也非常巧妙。

[圖片上傳失敗...(image-a0b490-1596530601127)]

大家都知道當(dāng)一個(gè)類被加載的時(shí)候,其方法都會(huì)被放在方法區(qū),而且同一個(gè)類的方法會(huì)被緊密地排列在一起,而我們可以拿到兩個(gè)方法的地址起始值,而這兩者之間的差值就是第一個(gè)方法結(jié)構(gòu)體的大小。正是基于這一點(diǎn),只要去構(gòu)造一個(gè)只有兩個(gè)靜態(tài)方法的類,那么就可以通過獲取這兩個(gè)方法的起始地址相減拿到方法對(duì)應(yīng)結(jié)構(gòu)體的大小,從而進(jìn)行整體的替換。

總之,Sophix及時(shí)修復(fù)的方法是非常巧妙的,既沒有用到插樁,同時(shí)又不需要考慮兼容性,在性能層面和兼容性層面都具有很好的保障。從及時(shí)修復(fù)的角度來看,Sophix的確有“四兩撥千斤”的功效。

3.2 Sophix冷啟動(dòng)修復(fù)原理解析
上面提到的及時(shí)修復(fù)只能針對(duì)方法體內(nèi)部結(jié)構(gòu)被修改的場景,而對(duì)于類本身結(jié)構(gòu)的改變,及時(shí)修復(fù)就沒有辦法了,這時(shí)候就需要用到冷啟動(dòng)修復(fù)。冷啟動(dòng)修復(fù)就是需要下發(fā)一個(gè)新的補(bǔ)丁,在補(bǔ)丁中會(huì)有一個(gè)新的補(bǔ)丁類,在App重啟的時(shí)候會(huì)優(yōu)先加載這個(gè)補(bǔ)丁類達(dá)到去替換原有Bug類的效果。

對(duì)于冷啟動(dòng)修復(fù)而言,針對(duì)于不同的虛擬機(jī)有不同的原則,Android主流的Dalvik和ART兩個(gè)虛擬機(jī),它們最大的區(qū)別就是是否支持多個(gè)dex文件的加載。ART也就是Android 5.0以上的虛擬機(jī)本身就支持多個(gè)dex文件加載,而Dalvik卻不支持多個(gè)dex加載,只支持一個(gè)dex加載,如果需要支持多個(gè)dex加載則需要引入multi-dex方案。而Dalvik和ART加載多個(gè)dex文件的不同卻決定了它們需要采用不同熱修復(fù)方案的原因。

[圖片上傳失敗...(image-a055d2-1596530601127)]

ART本身支持多個(gè)dex加載,所以在程序啟動(dòng)之前ART已經(jīng)把多個(gè)dex文件都加載進(jìn)來了,在運(yùn)行后所有的類都已經(jīng)被放在ClassLoader里面了,而不需要再去做加載工作。而Dalvik使用的multi-dex方案實(shí)際上在程序運(yùn)行之前只加載了classes.dex文件,而剩下的其他的dex文件都是在程序啟動(dòng)之后application運(yùn)行的時(shí)候再去加載的,所以可以看作在程序運(yùn)行時(shí)進(jìn)行dex加載,所以兩者之間存在著本質(zhì)的區(qū)別。

在做冷啟動(dòng)修復(fù)的時(shí)候,Sophix的根本原則就是非侵入式,不能對(duì)于App本身有任何改造,同時(shí)也要保證整個(gè)App的性能,所以不能使用插樁的方案,也必須要做到dex的全量替換,重新去執(zhí)行dex opt過程生成新的odex,再去把dexElement數(shù)組進(jìn)行全量替換,達(dá)到加載新的補(bǔ)丁的效果。

ART的冷啟動(dòng)修復(fù)
ART的冷啟動(dòng)方案是比較簡單的,因?yàn)锳RT本身就支持多個(gè)dex加載,當(dāng)然多個(gè)dex加載也是存在一定順序的,首先需要加載classes.dex。正是基于這樣的加載順序,當(dāng)patch.dex被下發(fā)到端上之后,只需要將其放到第一位,也就是將其文件名改為classes.dex,而將原來的文件名依次后移一位,然后重新執(zhí)行l(wèi)oadDex的加載過程,生成新的odex并全量替換原有的odex,這樣就可以保證補(bǔ)丁包dex文件被優(yōu)先加載,ART下的冷啟動(dòng)修復(fù)就是這樣實(shí)現(xiàn)的。

[圖片上傳失敗...(image-b7d88-1596530601127)]

這里進(jìn)行了一次整體的替換而不再只是某一個(gè)dex文件的插入,并且重新執(zhí)行了dex opt的過程,所以這里不會(huì)出現(xiàn)預(yù)校驗(yàn)的問題。同時(shí)以前和補(bǔ)丁包在同一個(gè)dex文件的這些類因?yàn)檠a(bǔ)丁包被打到了新的dex文件中,它們的預(yù)檢驗(yàn)標(biāo)簽會(huì)被去除掉,但是由于這些類的數(shù)量很少,所以對(duì)于性能的影響也是比較小的,做到了最大程度地降低了熱修復(fù)所帶來的性能開銷。而因?yàn)閘oadDex的過程是非常耗時(shí)的,所以在真正實(shí)現(xiàn)的時(shí)候會(huì)做通過異步的方式另外開辟一個(gè)線程去做dex與odex的轉(zhuǎn)換。當(dāng)App重新啟動(dòng),新的odex全部生成之后才會(huì)去做dexElement數(shù)組的替換,這就最大程度地保證了App運(yùn)行的穩(wěn)定性。因?yàn)闊嵝迯?fù)方案的目標(biāo)是幫助修復(fù)問題,所以不能再帶來其他的穩(wěn)定性問題,所以Sophix方案在這里花費(fèi)了很大精力進(jìn)行優(yōu)化。

Dalvik的冷啟動(dòng)修復(fù)
下表中除了Sophix還列舉了另外兩個(gè)方案進(jìn)行對(duì)比來看。

[圖片上傳失敗...(image-ec183e-1596530601127)]

這里Qxxx方案原理是dexElement注入,特點(diǎn)是實(shí)現(xiàn)比較簡單,但是由于基于插樁實(shí)現(xiàn),所以本身類加載的開銷比較大,所以不適用于生產(chǎn)環(huán)境。Txxx方案和Sophix都是全量地替換dex,同時(shí)Txxx也不使用插樁的方式,最大程度地保障了性能,最具有的特點(diǎn)的就是自研了dex合并算法,最大限度地減少了代碼體積,而這一點(diǎn)是它的優(yōu)勢也是它的劣勢,需要比較細(xì)粒度地做dex合并,從而可以把多個(gè)dex合并成為一個(gè)dex再去加載。但是這個(gè)自研的dex合并算法比較復(fù)雜,然而帶來的收益卻并不大,雖然減少了代碼體積,但是一個(gè)App的體積里面的大部分并不是代碼本身而是資源,所以通過合并算法減少的代碼體積只是很少的一部分,所以性價(jià)比很低。而在Sophix里面,雖然也做了全量的替換,但是相對(duì)而言比較簡單,以類為粒度合并dex,雖然沒有減少dex文件的體積,但是合并的方法更加簡單明了。

在冷啟動(dòng)修復(fù)下,Sophix方案有一個(gè)很簡單的思想就是當(dāng)發(fā)現(xiàn)某些類存在Bug下發(fā)新的補(bǔ)丁之后,如果把原有的存在Bug的類從原來所屬的dex摳出來再去執(zhí)行加載的時(shí)候,因?yàn)樵械膁ex文件不再有這些類了,此時(shí)就會(huì)去從patch.dex文件中加載到它,這樣就可以實(shí)現(xiàn)熱修復(fù)的效果,并且這樣并不會(huì)有預(yù)檢驗(yàn)的問題,從而最大程度地保證了程序性能。所以這個(gè)方案中最困難的一點(diǎn)就是如何把以前這些有Bug的類從dex中摳出來,這個(gè)問題可能會(huì)非常復(fù)雜,因?yàn)槊總€(gè)類的大小都不一樣,如何將其從連續(xù)排列的內(nèi)存空間中取出來然后再去做移位操作,這樣想起來很復(fù)雜。而實(shí)際上Sophix使用了一個(gè)很巧妙的方式實(shí)現(xiàn)這樣的事情。

[圖片上傳失敗...(image-ba510d-1596530601127)]

在dex文件中其實(shí)會(huì)有一個(gè)叫做dexHeader的結(jié)構(gòu),這就是dex文件的文件頭,在這里面有兩個(gè)成員與實(shí)現(xiàn)緊密相關(guān):一個(gè)叫做classDefsSize,另一個(gè)叫做classDefsOff。ClassDef也就是所有的類定義,這個(gè)dex文件里面所有涉及到的類都會(huì)被注冊(cè)到classDef的結(jié)構(gòu)體里面,classDefsOff指的就是classDef結(jié)構(gòu)的偏移量,所以通過classDefsOff就可以找到對(duì)應(yīng)的dex文件里面的classDef,從而找到這個(gè)dex文件中到底有哪些類,虛擬機(jī)也是同樣的原理,通過classDefsOff偏移量找到classDef再遍歷并加載相應(yīng)的類。而想要實(shí)現(xiàn)熱修復(fù)只需要在classDef里面將需要被替換的類抹掉就可以了。所以在Dalvik下面的邏輯就是通過DexHeader找到classDefsOff,再在classDefs里面找到需要修改的類,把這些類抹掉再去修改classDefsSize,這樣當(dāng)虛擬機(jī)再去加載dex文件的時(shí)候就會(huì)認(rèn)為被修復(fù)的類不包含在dex文件里面,盡管實(shí)際上這些類的實(shí)現(xiàn)還是在dex文件里面的。

[圖片上傳失敗...(image-4aa2f1-1596530601127)]

3.3 Sophix資源修復(fù)原理解析
之前在Instant Run里面也提到了資源修復(fù),因?yàn)橘Y源修復(fù)下發(fā)的是全量的資源包,所以并不適合在線上的環(huán)境中應(yīng)用。想要在線上環(huán)境做資源修復(fù)肯定會(huì)使用差量的資源包,下發(fā)更新過的資源,而Sophix也是這樣做的。

[圖片上傳失敗...(image-41885-1596530601127)]

之前提到所有資源的訪問都是被AssetManager類所代理的,AssetManager里面會(huì)有一個(gè)AssetPath數(shù)組,其中會(huì)有兩個(gè)成員,一個(gè)是指向系統(tǒng)框架資源,一個(gè)應(yīng)用的指向資源。Sophix資源修復(fù)的思路就是在AssetPath數(shù)組里面多加一個(gè)資源,這樣當(dāng)在前兩個(gè)資源路徑中都找不到時(shí)候就可以從新的資源路徑中尋找,可以通過這樣的方式去修復(fù)資源替換的問題。當(dāng)資源修改之后會(huì)下發(fā)一個(gè)資源修復(fù)的補(bǔ)丁包,同時(shí)把補(bǔ)丁包集成到端上之后會(huì)在AssetManager里面通過addAssetPath方法添加一個(gè)新的成員,進(jìn)而實(shí)現(xiàn)資源修復(fù)。

而Sophix的資源修復(fù)會(huì)涉及到以下三種情況:

  • 新增資源導(dǎo)致原有資源id偏移:對(duì)比新舊代碼前,將新包中所引用的未修改資源ID修正。
  • 引用內(nèi)容修改的資源:對(duì)比新舊代碼前,在新包中將所引用的原有資源ID置為更新后的ID。
  • 刪除資源:無需修改。

[圖片上傳失敗...(image-92c23b-1596530601127)]

四、熱修復(fù)方案的總結(jié)和對(duì)比

[圖片上傳失敗...(image-2e42c5-1596530601127)]

最后來總結(jié)和對(duì)比各種修復(fù)方案的原理和特點(diǎn):

  • Qxxx方案原理比較簡單,通過代碼插樁繞開預(yù)校驗(yàn)問題,此外通過dexElement插入的方式使得帶補(bǔ)丁的dex文件優(yōu)先加載。其優(yōu)點(diǎn)在于實(shí)現(xiàn)比較簡單,可以修復(fù)大部分類層面的問題。但是同時(shí)其問題也是比較突出的,第一點(diǎn)是不支持實(shí)時(shí)生效,第二點(diǎn)就是全量插樁的方式侵入性非常強(qiáng),同時(shí)性能損耗也是非常大的。
  • Instant Run方案原理同樣用到了代碼插樁,而且其插樁比Qxxx方案更加復(fù)雜,它還會(huì)用到宿主的application做很多準(zhǔn)備工作,這之后才會(huì)去執(zhí)行業(yè)務(wù)代碼,最后它還會(huì)去通過AssetManager重建做資源修復(fù)工作。優(yōu)點(diǎn)是它能同時(shí)支持方法更新、類更新和資源更新,并且在方法更新的過程中還可以做到及時(shí)修復(fù),不需要重啟App。其問題在于還是使用了全量插樁,所以侵入性很強(qiáng),同時(shí)對(duì)于性能的損耗也很大,由于在進(jìn)行修復(fù)時(shí)需要下發(fā)全量資源包,所以開銷非常大,同時(shí)也不適合在實(shí)際的生產(chǎn)環(huán)境中使用。
  • Andfix方案的原理是native方法的替換,這個(gè)方法很巧妙,并且實(shí)現(xiàn)比較簡單,而且可以做到及時(shí)生效。但是它不支持類結(jié)構(gòu)的改變,同時(shí)因?yàn)椴煌姹咎摂M機(jī)的方法體結(jié)構(gòu)不同,無法實(shí)現(xiàn)兼容性的處理,所以這種方案的兼容性也比較差。
  • Sophix方案使用了很多很巧妙的原理實(shí)現(xiàn),首先它還是使用native方法替換,這種方法會(huì)比Andfix更加巧妙,它不需要知道方法結(jié)構(gòu)體具體的成員變量,而直接使用整體的替換,只需要知道方法結(jié)構(gòu)體的大小即可。它使用了很巧妙的方式,通過兩個(gè)緊密排列的方法的地址差完成了方法替換即時(shí)生效的功能。Sophix使用全量dex替換去完成冷啟動(dòng)修復(fù)場景,而在資源修復(fù)的時(shí)候使用了差量資源包注入的方式,最大限度地降低了網(wǎng)絡(luò)的開銷,只需要很輕量地把差量資源包下發(fā)就可以了。其優(yōu)點(diǎn)在于同時(shí)支持方法更新、類更新和資源更新,而且包括native方法的替換以及資源包的注入等很多實(shí)現(xiàn)非常巧妙和優(yōu)雅,也非常輕量,并且屬于非侵入式的修復(fù)。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容