本文來自於騰訊bugly開發者社區,非經做者贊成,請勿轉載,原文地址:http://dev.qq.com/topic/57a31...python
Android 不只系統版本衆多,機型衆多,並且各個市場都各有各的政策和審覈速度,每次發佈一個版本對於開發同窗來說都是一種漫長的煎熬。相比於 iOS 兩三天就能達到 80% 的覆蓋速度而言,Android 應用版本升級至少須要兩週才能達到 80% 的升級率,嚴重阻礙了版本迭代速度。也致使市場上 App 版本分散,處理 bug 和投訴等也愈來愈麻煩。算法
修復的 bug 須要等待下個版本發佈窗口才能發佈?瀏覽器
已經 ready 的需求排隊上線,須要等待其餘 Feature Team 合入代碼?微信
老版本升級速度慢?頻繁上線版本提醒用戶升級,影響用戶體驗?多線程
這幾個問題是每一個 App 開發同窗都必然要面對的。那麼有沒有方法能在用戶無感知的狀況下加速 bug 處理和版本迭代速度?架構
在這方面 PC 端 Chrome 瀏覽器的 patch 升級方案給咱們了一個很好的借鑑:當 Chrome 有版本升級的時候會自動下載 patch 文件。下次啓動後,Chrome 就已是新版本。併發
近一兩年 Android 熱補丁框架很是熱門。從最初 360 動態下發 lua 腳本,到後來出現的各類方案,如雨後春筍般出現。早期的補丁框架偏向於以代碼修復爲主,主要分爲兩大類:native hook 方案和 Multidex 方案。app
native hook 方案如阿里巴巴的 AndFix 和 Dexposed。Multidex 方案如 Qzone。切入點都是替換掉將要執行的代碼。基於 Qzone 方案的思路,出現了 nuwa 這個比較完善的庫,工具鏈比較完善。框架
相似 Chrome 的 patch 升級方案足以知足加速 bug 處理和版本迭代速度的需求,給了咱們很大的借鑑意義。在安卓系統上,能夠經過 hotfix 的思路來達到這一目的:下發補丁文件,更新 App 版本。ide
在今年 3 月份開始作技術選型的時候把上面的幾種方案試了一輪。其中 AndFix 甚至跟上了現網的一個發佈版本,可是因爲影響正向開發過程(只能修改方法、不能修改 field、不能新增類等問題)、庫自己難於維護(須要依賴外部開源力量進行維護)以及發現的莫名其妙的 bug(致使咱們 App 下發 patch 後白屏),因此即便跟上了發佈版本也沒有使用。nuwa 僅支持更新 Java 代碼,不能更新資源和 so 文件,知足不了咱們的需求。
沒有好用的輪子,咱們決定本身造一個,因而有了如今的 patch 方案。
既然作安卓 patch 方案,最好的結果就是能支持更新 App 全部的代碼和資源。可是
Application
類是 App 啓動之初就被安卓系統加載起來,因此至少 Application
類和它啓動依賴的其餘業務類是不能被更新的?
修復 bug 或者版本迭代過程當中不免會遇到須要修改資源文件的狀況。資源文件能更新嗎?
native 實現的 so 文件如何更新?
針對上面三個問題, 咱們的設計是把 App 僅僅當作一個加載器。系統啓動 App 以後,加載器決定將要運行的代碼和資源的位置。當有新功能或者 bugfix 須要推送給用戶,替換加載器內容便可。
上面提到 Application
因爲啓動就被加載而不能被更新的問題,咱們代理了真實 Application
類的建立過程。經過代理 Application
,控制 Application
重新 dex 文件中加載。假設真實的 Application
類是 MyApplication
。咱們在編譯期間自動修改 AndroidManifest.xml
文件,把 MyApplication
替換爲 MoaiApplication
(是 App 的入口 Application)。App 啓動後由 MoaiApplication
加載完相應的文件(dex/資源文件/so 文件)後,再將控制權交回給 MyApplication
。
將控制權交回給 MyApplication
,咱們最初是代理 MyApplication
的生命週期。具體作法是,MoaiApplication
決定加載哪裏的業務代碼、資源文件以及 so 文件以後依然負責接收 App 的所有生命週期,而後把生命週期代理給 MyApplication
,簡單例子以下:
還有比較多生命週期函數上面代碼就沒一一列舉。
從上面代碼容易想到代理方案的缺點:必需要完整代理全部生命週期接口。不然 MyApplication
會因爲生命週期不完整而出現奇怪的 bug。好比咱們最第一版本在測試過程當中就出現了沒有代理 registerActivityLifecycleCallbacks
函數而致使拿不到 Activity
生命週期 onActivityCreated
/onActivityDestroyed
等回調。
踩到生命週期回調不完整的坑以後,咱們開始考慮能不能把 App 運行期間 Application
的引用所有替換成 MyApplication
?這樣就無需 MoaiApplication
把生命週期代理給 MyApplication
,而是由 MyApplication
直接接收系統回調。安卓系統 ContextWrapper
的實現是包裝了一層真正的 mBase
上下文,App 真正使用到的就是這個 mBase
。經過反射 mBase
以及其中字段對 Application
的引用,『完全』解決了須要手寫代理 Application
所有生命週期的方法。
Qzone 方案下發的 patch 文件是變動過的 Java 類組成的 patch.dex,在 dalvik 和 ART 虛擬機下分別須要解決 Class ref in pre-verified class resolved to unexpected implementation
和內存地址錯亂問題。這些問題根源在於改變了類本來所屬的 dex 文件。既然改變類所在的 dex 會致使各類各樣的問題,那直接替換掉整個 dex 不就行了?在調研 JRebal for Android 和 Instant Run 的時候也發現了他們有相似的作法。
咱們把 App 的 dex 分紅兩部分:
patch 庫的 dex 文件 -> classes.dex
其餘業務代碼的 dex 文件 -> classes[N].dex
其中 classes.dex 中僅包含了 patch 庫的所有代碼,並不包含任何其餘業務代碼。
假設 apk 中包含三個文件:classes.dex、classes2.dex、classes3.dex。classes.dex 充當的角色就是加載器,負責啓動 App 而且加載後面的兩個 dex。這樣作的目的是,App 啓動須要用到的全部類都集中在 classes.dex 中,全部業務代碼的類都集中在 classes[N].dex 中。若是某次下發 patch 代碼把 classes2.dex 變動爲 classes2-1.dex,那麼由加載器加載 classes2-1.dex 和 classes3.dex 便可實現更新包含 MyApplication
類在內的全部代碼。
若是 dex 文件有更新,加載器會選擇加載更新後的文件。咱們最初採用了 Google 官方的 Multidex 方案,擴展 DexPathList
的 dexElements
字段。
Multidex 方案上線後發現某些機型(好比三星s6 5.0.2 ROM)並不能加載擴展進去的 dex 中的代碼。debug 階段卻能順利加載(debugger 拖慢代碼執行速度)。目前的猜想是某些廠商在 5.x 以上版本改動 ROM 致使 App 啓動邏輯有多線程併發執行。
最終咱們棄用了 Multidex 方案,轉而 Hack 系統 ClassLoader。
全部線程使用的是同一個 ClassLoader
對象。因此一旦 Hack 了這個對象,全部線程都開始使用 Hack 過的對象,從而可以解決多線程致使加載不到擴展的 dex 文件中代碼的問題。
安卓系統加載代碼的 ClassLoader
是 PathClassLoader
和 BootClassLoader
。咱們最初設計的方案是在 PathClassLoader
和 BootClassLoader
之間插入一個 BaseDexClassLoader
,讓全部業務代碼都在這個插入的 BaseDexClassLoader
中加載。可是這樣的設計存在缺陷:業務代碼的 ClassLoader
會變成 BaseDexClassLoader
,若是業務代碼依賴了 patch 庫的代碼(在 classes.dex 中),會出現 ClassNotFoundException
。
在這方面 Instant Run 的設計很精巧。它讓 PathClassLoader
插入的父 loader (IncrementalClassLoader
)包裝了 DelegateClassLoader
,而且把 DelegateClassLoader
的父 loader 設置爲 PathClassLoader
,使得類加載的路徑變成:
在 DelegateClassLoader
加載業務代碼的時候(業務代碼在 classesN.dex 中),流程會沿着標記的順序最終第 5 步成功加載到業務代碼。業務代碼若是依賴 patch 庫的代碼,會在 PathClassLoader
加載。這樣全部代碼均可以被加載到。
單純更新 Java 代碼的 patch 框架,實用性會受到很大的侷限。開發同窗須要仔細驗證提交內容,確保提交中不包含資源文件的變動以及 native so 的改動,會致使本就複雜的開發流程變得更加繁瑣。因此咱們在支持更新 Java 代碼的基礎之上,也支持更新資源和 native so 文件。
App 加載資源是依賴 Context#getResources
函數返回的 Resources
對象。Resources
內部包裝了 AssetManager
,最終由 AssetManager
從 apk 文件中加載資源。因此咱們反射了替換系統默認的 Resources
,讓 AssetManager
從咱們更新後的 apk 中加載資源。現階段的實現支持好比 string/anim/drawable/color/layout 等資源文件的變動。因爲 Android 系統在安裝 apk 時候已經把 AndroidManifest.xml
文件解析並寫入到系統中,目前還不支持修改四大組件,好比增長 Activity
。後續會繼續研究如何作到無縫修改四大組件。
在 Android 項目中使用 native 函數前須要先調用 System.loadLibrary(libName)
。
當 lib 文件須要更新或者有 bug 時候怎麼辦?首先想到的是在代碼中把加載 so 文件的代碼改爲System.load(libFilePath)
,讓系統加載本身指定的 libFilePath
文件。然而這樣的改動須要
在源代碼中修改或者使用工具在編譯期把 loadLibrary
接口改成 load
patch 庫把 so 文件從 patch 文件中複製到特定目錄
這樣在運行期纔有可能加載更新後的 so 文件。
經過分析系統加載 so 文件的方式後,咱們使用了更簡單的處理方法。查找 lib 文件是經過調用 PathClassLoader
的 findLibrary
,最終調用到 DexPathList
的 findLibrary
。DexPathList
會在本身維護的列表目錄中查找對應的 lib 文件是否存在。因此咱們在發現 patch 文件中有 so 文件變動的時候,會在 PathClassLoader
的 nativeLibraryDirectories
(Android6.0如下)或者nativeLibraryPathElements
(Android 6.0及以上)的最前面插入自定義的lib文件目錄。這樣 ClassLoader
在 findLibrary
的時候會先在自定義的 lib 目錄中查找,優先加載變動過的 so 文件。
回到咱們最初的目標:patch 不該該影響正向開發流程。咱們生成 patch 文件是針對 apk 進行的,開發同窗無需關心這次發佈是 patch 版本仍是正常版本,只須要正常開發而且打包要發佈的 apk 便可,不會對正向開發流程產生任何影響。
咱們提供 python 腳本生成兩個 apk 的:對比兩個 apk 中的全部文件,找出有變動的文件進行 diff,把 diff 結果寫入 patch 文件。線上用戶下載 patch 文件到本地以後,啓動一條新的進程使用 context.getApplicationInfo().sourceDir
路徑的 apk 與 patch 文件合併,獲得新的 apk(包含資源文件,不包含 dex 文件)以及 dex 文件、native so 文件,並在這條進程中提早作 dex 優化(dex2oat/dexopt)。針對 dex 優化過程太慢的問題(優化過程慢會致使進程可能會系統kill,下降 patch 成功率)咱們併發了 dex 優化過程,使 patch 過程耗時相對減少。新 apk、dex文件、so 文件就能夠在下次啓動 App 的時候由加載器加載。
正所謂沒有完美的架構,只有適合本身的架構。當前的開源方案並不能知足咱們加速 bug處理和版本迭代速度的需求,因而有了站在巨人肩膀上的思考和咱們如今的 patch 方案。咱們目前的優點:
全面支持 patch Java 代碼、資源文件 和 native so 文件。版本只須要正常滾動,開發同窗無需關心是發佈 patch 版本仍是正常版本
使用相對簡單(減小接入成本也是咱們的最初思考點之一),只須要在 build.gradle 中加入三行代碼便可,無需更多配置。
從咱們團隊發佈的多個 patch 版原本看,下發的 diff 結果文件稍大。大文件下載過程可能出現的錯誤也會間接影響到 patch 鋪開的速度,因此咱們也在嘗試更好的 diff 方案。Chrome 最初升級方案也是 bsdiff,然後慢慢演變出 Courgette 算法。
咱們對於補丁框架的定義不只僅是『修復bug』就足夠,除此以外,如何快速接入,如何作到不影響現有流程,這對於不少應用來講相當重要。在此之上,搞清楚框架的定位,適當捨棄一些不重要方面的時候,快速迭代,在迭代中持續優化,事情每每比想象的更加簡單。
持續交付一直都是快速迭代思想的一種踐行方式,對於 App 開發而言,若是咱們經過構造補丁框架這樣一個渠道,能夠經過自動化系統把補丁快速地把新功能推送給用戶,那這個事情的意義就不只僅是『修復 bug』這麼簡單。減小線上 crash 率和加速版本迭代、讓新功能儘早與用戶見面,從而能夠在更短的時間內不斷收集用戶反饋信息對產品進行打磨。
目前咱們已經在微信讀書線上三個版本開始試行了用補丁代替版本發佈或者加速老版本升級的作法,期待未來能經過這個渠道,爲安卓開發同窗們作到無感知的持續交付過程 。
更多精彩內容歡迎關注bugly的微信公衆帳號:
騰訊 Bugly是一款專爲移動開發者打造的質量監控工具,幫助開發者快速,便捷的定位線上應用崩潰的狀況以及解決方案。智能合併功能幫助開發同窗把天天上報的數千條 Crash 根據根因合併分類,每日日報會列出影響用戶數最多的崩潰,精準定位功能幫助開發同窗定位到出問題的代碼行,實時上報能夠在發佈後快速的瞭解應用的質量狀況,適配最新的 iOS, Android 官方操做系統,鵝廠的工程師都在使用,快來加入咱們吧!