Android熱更新機(jī)制

轉(zhuǎn)載自http://dev.qq.com/topic/57a31921ac3a1fb613dd40f3

Android 不僅系統(tǒng)版本眾多,機(jī)型眾多,而且各個(gè)市場(chǎng)都各有各的政策和審核速度,每次發(fā)布一個(gè)版本對(duì)于開發(fā)同學(xué)來講都是一種漫長(zhǎng)的煎熬。相比于 iOS 兩三天就能達(dá)到 80% 的覆蓋速度而言,Android 應(yīng)用版本升級(jí)至少需要兩周才能達(dá)到 80% 的升級(jí)率,嚴(yán)重阻礙了版本迭代速度及穗。也導(dǎo)致市場(chǎng)上 App 版本分散,處理 bug 和投訴等也越來越麻煩绵载。

  • 修復(fù)的 bug 需要等待下個(gè)版本發(fā)布窗口才能發(fā)布埂陆?
  • 已經(jīng) ready 的需求排隊(duì)上線,需要等待其他 Feature Team 合入代碼尘分?
  • 老版本升級(jí)速度慢猜惋?頻繁上線版本提醒用戶升級(jí),影響用戶體驗(yàn)培愁?

這幾個(gè)問題是每個(gè) App 開發(fā)同學(xué)都必然要面對(duì)的著摔。那么有沒有方法能在用戶無感知的情況下加速 bug 處理和版本迭代速度?
在這方面 PC 端 Chrome 瀏覽器的 patch 升級(jí)方案給我們了一個(gè)很好的借鑒:當(dāng) Chrome 有版本升級(jí)的時(shí)候會(huì)自動(dòng)下載 patch 文件定续。下次啟動(dòng)后谍咆,Chrome 就已經(jīng)是新版本。

他山之石私股,可以攻玉

近一兩年 Android 熱補(bǔ)丁框架非常熱門摹察。從最初 360 動(dòng)態(tài)下發(fā) lua 腳本,到后來出現(xiàn)的各種方案倡鲸,如雨后春筍般出現(xiàn)供嚎。早期的補(bǔ)丁框架偏向于以代碼修復(fù)為主,主要分為兩大類:native hook 方案和 Multidex 方案。

native hook 方案如阿里巴巴的 AndFix 和 Dexposed克滴。Multidex 方案如 Qzone逼争。切入點(diǎn)都是替換掉將要執(zhí)行的代碼∪芭猓基于 Qzone 方案的思路誓焦,出現(xiàn)了 nuwa 這個(gè)比較完善的庫,工具鏈比較完善着帽。

類似 Chrome 的 patch 升級(jí)方案足以滿足加速 bug 處理和版本迭代速度的需求杂伟,給了我們很大的借鑒意義。在安卓系統(tǒng)上仍翰,可以通過 hotfix 的思路來達(dá)到這一目的:下發(fā)補(bǔ)丁文件赫粥,更新 App 版本。

站在巨人的肩膀上

在今年 3 月份開始做技術(shù)選型的時(shí)候把上面的幾種方案試了一輪歉备。其中 AndFix 甚至跟上了現(xiàn)網(wǎng)的一個(gè)發(fā)布版本傅是,但是由于影響正向開發(fā)過程(只能修改方法、不能修改 field蕾羊、不能新增類等問題)、庫本身難于維護(hù)(需要依賴外部開源力量進(jìn)行維護(hù))以及發(fā)現(xiàn)的莫名其妙的 bug(導(dǎo)致我們 App 下發(fā) patch 后白屏)帽驯,所以即使跟上了發(fā)布版本也沒有使用龟再。nuwa 僅支持更新 Java 代碼,不能更新資源和 so 文件尼变,滿足不了我們的需求利凑。

沒有好用的輪子,我們決定自己造一個(gè)嫌术,于是有了現(xiàn)在的 patch 方案哀澈。

App 只是一個(gè)加載器

既然做安卓 patch 方案,最好的結(jié)果就是能支持更新 App 所有的代碼和資源度气。但是

  • Application類是 App 啟動(dòng)之初就被安卓系統(tǒng)加載起來割按,所以至少 Application類和它啟動(dòng)依賴的其他業(yè)務(wù)類是不能被更新的?
  • 修復(fù) bug 或者版本迭代過程中難免會(huì)遇到需要修改資源文件的情況磷籍。資源文件能更新嗎适荣?
  • native 實(shí)現(xiàn)的 so 文件如何更新?

針對(duì)上面三個(gè)問題院领, 我們的設(shè)計(jì)是把 App 僅僅當(dāng)做一個(gè)加載器弛矛。系統(tǒng)啟動(dòng) App 之后,加載器決定將要運(yùn)行的代碼和資源的位置比然。當(dāng)有新功能或者 bugfix 需要推送給用戶丈氓,替換加載器內(nèi)容即可。

支持更新全部代碼

上面提到Application由于啟動(dòng)就被加載而不能被更新的問題,我們代理了真實(shí)Application類的創(chuàng)建過程万俗。通過代理Application湾笛,控制Application從新 dex 文件中加載。假設(shè)真實(shí)的Application類是MyApplication该编。我們?cè)诰幾g期間自動(dòng)修改AndroidManifest.xml文件迄本,把MyApplication替換為MoaiApplication(是 App 的入口 Application)。App 啟動(dòng)后由MoaiApplication加載完相應(yīng)的文件(dex/資源文件/so 文件)后课竣,再將控制權(quán)交回給MyApplication嘉赎。

代理生命周期

將控制權(quán)交回給MyApplication,我們最初是代理MyApplication的生命周期于樟。具體做法是公条,MoaiApplication
決定加載哪里的業(yè)務(wù)代碼、資源文件以及 so 文件之后依然負(fù)責(zé)接收 App 的全部生命周期迂曲,然后把生命周期代理給MyApplication靶橱,簡(jiǎn)單例子如下:


還有比較多生命周期函數(shù)上面代碼就沒一一列舉。
從上面代碼容易想到代理方案的缺點(diǎn):必須要完整代理所有生命周期接口路捧。否則MyApplication會(huì)由于生命周期不完整而出現(xiàn)奇怪的 bug关霸。比如我們最初版本在測(cè)試過程中就出現(xiàn)了沒有代registerActivityLifecycleCallbacks函數(shù)而導(dǎo)致拿不到Activity生命周期onActivityCreated/onActivityDestroyed等回調(diào)。

反射 Application

踩到生命周期回調(diào)不完整的坑之后杰扫,我們開始考慮能不能把 App 運(yùn)行期間Application的引用全部替換成MyApplication队寇?這樣就無需MoaiApplication把生命周期代理給MyApplication,而是由MyApplication直接接收系統(tǒng)回調(diào)章姓。安卓系統(tǒng)ContextWrapper的實(shí)現(xiàn)是包裝了一層真正的mBase上下文佳遣,App 真正使用到的就是這個(gè)mBase。通過反射mBase以及其中字段對(duì)Application的引用凡伊,『徹底』解決了需要手寫代理Application
全部生命周期的方法零渐。

dex分包

Qzone 方案下發(fā)的 patch 文件是變更過的 Java 類組成的 patch.dex,在 dalvik 和 ART 虛擬機(jī)下分別需要解決Class ref in pre-verified class resolved to unexpected implementation

和內(nèi)存地址錯(cuò)亂問題系忙。這些問題根源在于改變了類原本所屬的 dex 文件诵盼。既然改變類所在的 dex 會(huì)導(dǎo)致各種各樣的問題,那直接替換掉整個(gè) dex 不就好了笨觅?在調(diào)研 JRebal for Android 和 Instant Run 的時(shí)候也發(fā)現(xiàn)了他們有類似的做法拦耐。
我們把 App 的 dex 分成兩部分:

  • patch 庫的 dex 文件 -> classes.dex
  • 其他業(yè)務(wù)代碼的 dex 文件 -> classes[N].dex

其中 classes.dex 中僅包含了 patch 庫的全部代碼,并不包含任何其他業(yè)務(wù)代碼见剩。
假設(shè) apk 中包含三個(gè)文件:classes.dex杀糯、classes2.dex、classes3.dex苍苞。classes.dex 充當(dāng)?shù)慕巧褪?strong>加載器固翰,負(fù)責(zé)啟動(dòng) App 并且加載后面的兩個(gè) dex狼纬。這樣做的目的是,App 啟動(dòng)需要用到的所有類都集中在 classes.dex 中骂际,所有業(yè)務(wù)代碼的類都集中在 classes[N].dex 中疗琉。如果某次下發(fā) patch 代碼把 classes2.dex 變更為 classes2-1.dex,那么由加載器加載 classes2-1.dex 和 classes3.dex 即可實(shí)現(xiàn)更新包含MyApplication
類在內(nèi)的所有代碼歉铝。

怎么加載更新后的代碼盈简?

如果 dex 文件有更新,加載器會(huì)選擇加載更新后的文件太示。我們最初采用了 Google 官方的 Multidex 方案柠贤,擴(kuò)展DexPathList的dexElements字段。

Multidex 方案存在問題

Multidex 方案上線后發(fā)現(xiàn)某些機(jī)型(比如三星s6 5.0.2 ROM)并不能加載擴(kuò)展進(jìn)去的 dex 中的代碼类缤。debug 階段卻能順利加載(debugger 拖慢代碼執(zhí)行速度)臼勉。目前的猜測(cè)是某些廠商在 5.x 以上版本改動(dòng) ROM 導(dǎo)致 App 啟動(dòng)邏輯有多線程并發(fā)執(zhí)行。

最終我們棄用了 Multidex 方案餐弱,轉(zhuǎn)而 Hack 系統(tǒng) ClassLoader宴霸。

ClassLoader Hack 方案

所有線程使用的是同一個(gè)ClassLoader對(duì)象。所以一旦 Hack 了這個(gè)對(duì)象膏蚓,所有線程都開始使用 Hack 過的對(duì)象瓢谢,從而能夠解決多線程導(dǎo)致加載不到擴(kuò)展的 dex 文件中代碼的問題。

安卓系統(tǒng)加載代碼的ClassLoader是PathClassLoader和BootClassLoader驮瞧。我們最初設(shè)計(jì)的方案是在PathClassLoader和BootClassLoader之間插入一個(gè)BaseDexClassLoader恩闻,讓所有業(yè)務(wù)代碼都在這個(gè)插入的BaseDexClassLoader中加載。但是這樣的設(shè)計(jì)存在缺陷:業(yè)務(wù)代碼的ClassLoader會(huì)變成BaseDexClassLoader剧董,如果業(yè)務(wù)代碼依賴了 patch 庫的代碼(在 classes.dex 中),會(huì)出現(xiàn)ClassNotFoundException破停。

在這方面 Instant Run 的設(shè)計(jì)很精巧翅楼。它讓PathClassLoader插入的父 loader (IncrementalClassLoader)包裝了DelegateClassLoader,并且把DelegateClassLoader的父 loader 設(shè)置為PathClassLoader真慢,使得類加載的路徑變成:



在DelegateClassLoader加載業(yè)務(wù)代碼的時(shí)候(業(yè)務(wù)代碼在 classesN.dex 中)毅臊,流程會(huì)沿著標(biāo)記的順序最終第 5 步成功加載到業(yè)務(wù)代碼。業(yè)務(wù)代碼如果依賴 patch 庫的代碼黑界,會(huì)在PathClassLoader加載管嬉。這樣所有代碼都可以被加載到。

怎么更新資源朗鸠?

單純更新 Java 代碼的 patch 框架蚯撩,實(shí)用性會(huì)受到很大的局限。開發(fā)同學(xué)需要仔細(xì)驗(yàn)證提交內(nèi)容烛占,確保提交中不包含資源文件的變更以及 native so 的改動(dòng)胎挎,會(huì)導(dǎo)致本就復(fù)雜的開發(fā)流程變得更加繁瑣沟启。所以我們?cè)谥С指?Java 代碼的基礎(chǔ)之上,也支持更新資源和 native so 文件犹菇。

App 加載資源是依賴Context#getResources函數(shù)返回的Resources對(duì)象德迹。Resources內(nèi)部包裝了AssetManager,最終由AssetManager從 apk 文件中加載資源揭芍。所以我們反射了替換系統(tǒng)默認(rèn)的Resources胳搞,讓AssetManager從我們更新后的 apk 中加載資源。現(xiàn)階段的實(shí)現(xiàn)支持比如 string/anim/drawable/color/layout 等資源文件的變更称杨。由于 Android 系統(tǒng)在安裝 apk 時(shí)候已經(jīng)把AndroidManifest.xml文件解析并寫入到系統(tǒng)中肌毅,目前還不支持修改四大組件,比如增加Activity列另。后續(xù)會(huì)繼續(xù)研究如何做到無縫修改四大組件芽腾。

怎么更新 so 文件?

在 Android 項(xiàng)目中使用 native 函數(shù)前需要先調(diào)用System.loadLibrary(libName)页衙。

當(dāng) lib 文件需要更新或者有 bug 時(shí)候怎么辦摊滔?首先想到的是在代碼中把加載 so 文件的代碼改成System.load(libFilePath)
,讓系統(tǒng)加載自己指定的libFilePath文件店乐。然而這樣的改動(dòng)需要

  • 在源代碼中修改或者使用工具在編譯期把 loadLibrary接口改為 load
  • patch 庫把 so 文件從 patch 文件中復(fù)制到特定目錄

這樣在運(yùn)行期才有可能加載更新后的 so 文件艰躺。

通過分析系統(tǒng)加載 so 文件的方式后,我們使用了更簡(jiǎn)單的處理方法眨八。查找 lib 文件是通過調(diào)用PathClassLoader的findLibrary腺兴,最終調(diào)用到DexPathList的findLibrary。DexPathList會(huì)在自己維護(hù)的列表目錄中查找對(duì)應(yīng)的 lib 文件是否存在廉侧。所以我們?cè)诎l(fā)現(xiàn) patch 文件中有 so 文件變更的時(shí)候页响,會(huì)在PathClassLoader的nativeLibraryDirectories(Android6.0以下)或者nativeLibraryPathElements(Android 6.0及以上)的最前面插入自定義的lib文件目錄。這樣ClassLoader在findLibrary的時(shí)候會(huì)先在自定義的 lib 目錄中查找段誊,優(yōu)先加載變更過的 so 文件闰蚕。

patch 包的生成與應(yīng)用

回到我們最初的目標(biāo):patch 不應(yīng)該影響正向開發(fā)流程。我們生成 patch 文件是針對(duì) apk 進(jìn)行的连舍,開發(fā)同學(xué)無需關(guān)心此次發(fā)布是 patch 版本還是正常版本没陡,只需要正常開發(fā)并且打包要發(fā)布的 apk 即可,不會(huì)對(duì)正向開發(fā)流程產(chǎn)生任何影響索赏。

我們提供 python 腳本生成兩個(gè) apk 的:對(duì)比兩個(gè) apk 中的所有文件盼玄,找出有變更的文件進(jìn)行 diff,把 diff 結(jié)果寫入 patch 文件潜腻。線上用戶下載 patch 文件到本地之后埃儿,啟動(dòng)一條新的進(jìn)程使用context.getApplicationInfo().sourceDir路徑的 apk 與 patch 文件合并,得到新的 apk(包含資源文件砾赔,不包含 dex 文件)以及 dex 文件蝌箍、native so 文件青灼,并在這條進(jìn)程中提前做 dex 優(yōu)化(dex2oat/dexopt)。針對(duì) dex 優(yōu)化過程太慢的問題(優(yōu)化過程慢會(huì)導(dǎo)致進(jìn)程可能會(huì)系統(tǒng)kill妓盲,降低 patch 成功率)我們并發(fā)了 dex 優(yōu)化過程杂拨,使 patch 過程耗時(shí)相對(duì)減小。新 apk悯衬、dex文件弹沽、so 文件就可以在下次啟動(dòng) App 的時(shí)候由加載器加載。

優(yōu)勢(shì)和不足

正所謂沒有完美的架構(gòu)筋粗,只有適合自己的架構(gòu)策橘。當(dāng)前的開源方案并不能滿足我們加速 bug處理和版本迭代速度的需求,于是有了站在巨人肩膀上的思考和我們現(xiàn)在的 patch 方案娜亿。我們目前的優(yōu)勢(shì):

  • 全面支持 patch Java 代碼丽已、資源文件 和 native so 文件。版本只需要正常滾動(dòng)买决,開發(fā)同學(xué)無需關(guān)心是發(fā)布 patch 版本還是正常版本
  • 使用相對(duì)簡(jiǎn)單(減少接入成本也是我們的最初思考點(diǎn)之一)沛婴,只需要在 build.gradle 中加入三行代碼即可,無需更多配置督赤。

從我們團(tuán)隊(duì)發(fā)布的多個(gè) patch 版本來看嘁灯,下發(fā)的 diff 結(jié)果文件稍大。大文件下載過程可能出現(xiàn)的錯(cuò)誤也會(huì)間接影響到 patch 鋪開的速度躲舌,所以我們也在嘗試更好的 diff 方案丑婿。Chrome 最初升級(jí)方案也是 bsdiff,而后慢慢演變出 Courgette 算法没卸。

演進(jìn)與思考

我們對(duì)于補(bǔ)丁框架的定義不僅僅是『修復(fù)bug』就足夠羹奉,除此之外,如何快速接入约计,如何做到不影響現(xiàn)有流程尘奏,這對(duì)于很多應(yīng)用來說至關(guān)重要。在此之上病蛉,搞清楚框架的定位,適當(dāng)舍棄一些不重要方面的時(shí)候瑰煎,快速迭代铺然,在迭代中持續(xù)優(yōu)化,事情往往比想象的更加簡(jiǎn)單酒甸。

持續(xù)交付一直都是快速迭代思想的一種踐行方式魄健,對(duì)于 App 開發(fā)而言,如果我們通過構(gòu)造補(bǔ)丁框架這樣一個(gè)渠道插勤,可以通過自動(dòng)化系統(tǒng)把補(bǔ)丁快速地把新功能推送給用戶沽瘦,那這個(gè)事情的意義就不僅僅是『修復(fù) bug』這么簡(jiǎn)單革骨。減少線上 crash 率和加速版本迭代、讓新功能盡早與用戶見面析恋,從而可以在更短的時(shí)間內(nèi)不斷收集用戶反饋信息對(duì)產(chǎn)品進(jìn)行打磨良哲。

目前我們已經(jīng)在微信讀書線上三個(gè)版本開始試行了用補(bǔ)丁代替版本發(fā)布或者加速老版本升級(jí)的做法,期待將來能通過這個(gè)渠道助隧,為安卓開發(fā)同學(xué)們做到無感知的持續(xù)交付過程 筑凫。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市并村,隨后出現(xiàn)的幾起案子巍实,更是在濱河造成了極大的恐慌,老刑警劉巖哩牍,帶你破解...
    沈念sama閱讀 217,509評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件棚潦,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡膝昆,警方通過查閱死者的電腦和手機(jī)丸边,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來外潜,“玉大人原环,你說我怎么就攤上這事〈” “怎么了嘱吗?”我有些...
    開封第一講書人閱讀 163,875評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)滔驾。 經(jīng)常有香客問我谒麦,道長(zhǎng),這世上最難降的妖魔是什么哆致? 我笑而不...
    開封第一講書人閱讀 58,441評(píng)論 1 293
  • 正文 為了忘掉前任绕德,我火速辦了婚禮,結(jié)果婚禮上摊阀,老公的妹妹穿的比我還像新娘耻蛇。我一直安慰自己,他們只是感情好胞此,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,488評(píng)論 6 392
  • 文/花漫 我一把揭開白布臣咖。 她就那樣靜靜地躺著,像睡著了一般漱牵。 火紅的嫁衣襯著肌膚如雪夺蛇。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,365評(píng)論 1 302
  • 那天酣胀,我揣著相機(jī)與錄音刁赦,去河邊找鬼娶聘。 笑死,一個(gè)胖子當(dāng)著我的面吹牛甚脉,可吹牛的內(nèi)容都是我干的丸升。 我是一名探鬼主播,決...
    沈念sama閱讀 40,190評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼宦焦,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼发钝!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起波闹,我...
    開封第一講書人閱讀 39,062評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤酝豪,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后精堕,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體孵淘,經(jīng)...
    沈念sama閱讀 45,500評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,706評(píng)論 3 335
  • 正文 我和宋清朗相戀三年歹篓,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了瘫证。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,834評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡庄撮,死狀恐怖背捌,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情洞斯,我是刑警寧澤毡庆,帶...
    沈念sama閱讀 35,559評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站烙如,受9級(jí)特大地震影響么抗,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜亚铁,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,167評(píng)論 3 328
  • 文/蒙蒙 一蝇刀、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧徘溢,春花似錦吞琐、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至施蜜,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間雌隅,已是汗流浹背翻默。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評(píng)論 1 269
  • 我被黑心中介騙來泰國打工缸沃, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人修械。 一個(gè)月前我還...
    沈念sama閱讀 47,958評(píng)論 2 370
  • 正文 我出身青樓趾牧,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親肯污。 傳聞我的和親對(duì)象是個(gè)殘疾皇子翘单,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,779評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容