【騰訊Bugly干貨分享】Android Patch 方案與持續交付

本文來自于騰訊bugly開發者社區,非經作者同意,請勿轉載,原文地址:http://dev.qq.com/topic/57a31921ac3a1fb613dd40f3

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 方案。

native hook 方案如阿里巴巴的 AndFix 和 Dexposed。Multidex 方案如 Qzone。切入點都是替換掉將要執行的代碼。基于 Qzone 方案的思路,出現了 nuwa 這個比較完善的庫,工具鏈比較完善。

類似 Chrome 的 patch 升級方案足以滿足加速 bug 處理和版本迭代速度的需求,給了我們很大的借鑒意義。在安卓系統上,可以通過 hotfix 的思路來達到這一目的:下發補丁文件,更新 App 版本。

站在巨人的肩膀上

在今年 3 月份開始做技術選型的時候把上面的幾種方案試了一輪。其中 AndFix 甚至跟上了現網的一個發布版本,但是由于影響正向開發過程(只能修改方法、不能修改 field、不能新增類等問題)、庫本身難于維護(需要依賴外部開源力量進行維護)以及發現的莫名其妙的 bug(導致我們 App 下發 patch 后白屏),所以即使跟上了發布版本也沒有使用。nuwa 僅支持更新 Java 代碼,不能更新資源和 so 文件,滿足不了我們的需求。

沒有好用的輪子,我們決定自己造一個,于是有了現在的 patch 方案。

App 只是一個加載器

既然做安卓 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 等回調。

反射 Application

踩到生命周期回調不完整的坑之后,我們開始考慮能不能把 App 運行期間 Application 的引用全部替換成 MyApplication ?這樣就無需 MoaiApplication 把生命周期代理給 MyApplication,而是由 MyApplication 直接接收系統回調。安卓系統 ContextWrapper 的實現是包裝了一層真正的 mBase 上下文,App 真正使用到的就是這個 mBase。通過反射 mBase 以及其中字段對 Application 的引用,『徹底』解決了需要手寫代理 Application 全部生命周期的方法。

dex分包

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 方案,擴展 DexPathListdexElements 字段。

Multidex 方案存在問題

Multidex 方案上線后發現某些機型(比如三星s6 5.0.2 ROM)并不能加載擴展進去的 dex 中的代碼。debug 階段卻能順利加載(debugger 拖慢代碼執行速度)。目前的猜測是某些廠商在 5.x 以上版本改動 ROM 導致 App 啟動邏輯有多線程并發執行。

最終我們棄用了 Multidex 方案,轉而 Hack 系統 ClassLoader。

ClassLoader Hack 方案

所有線程使用的是同一個 ClassLoader 對象。所以一旦 Hack 了這個對象,所有線程都開始使用 Hack 過的對象,從而能夠解決多線程導致加載不到擴展的 dex 文件中代碼的問題。

安卓系統加載代碼的 ClassLoaderPathClassLoaderBootClassLoader。我們最初設計的方案是在 PathClassLoaderBootClassLoader 之間插入一個 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。后續會繼續研究如何做到無縫修改四大組件。

怎么更新 so 文件?

在 Android 項目中使用 native 函數前需要先調用 System.loadLibrary(libName)

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

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

這樣在運行期才有可能加載更新后的 so 文件。

通過分析系統加載 so 文件的方式后,我們使用了更簡單的處理方法。查找 lib 文件是通過調用 PathClassLoaderfindLibrary,最終調用到 DexPathListfindLibraryDexPathList 會在自己維護的列表目錄中查找對應的 lib 文件是否存在。所以我們在發現 patch 文件中有 so 文件變更的時候,會在 PathClassLoadernativeLibraryDirectories(Android6.0以下)或者nativeLibraryPathElements (Android 6.0及以上)的最前面插入自定義的lib文件目錄。這樣 ClassLoaderfindLibrary 的時候會先在自定義的 lib 目錄中查找,優先加載變更過的 so 文件。

patch 包的生成與應用

回到我們最初的目標: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 官方操作系統,鵝廠的工程師都在使用,快來加入我們吧!

?

轉載于:https://www.cnblogs.com/bugly/p/5740807.html

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/372547.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/372547.shtml
英文地址,請注明出處:http://en.pswp.cn/news/372547.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

App Engine中的Google Services身份驗證,第1部分

這篇文章將說明如何構建一個簡單的Google App Engine(GAE)Java應用程序,該應用程序可針對Google進行身份驗證,并利用Google的OAuth授權訪問Google的API服務(例如Google Docs)。 此外,在Google已…

c語言中空格的作用是什么,C語言中空格和\0的區別

\0 為字符串結束符,比如說:abcd (空格)cdefg;存入數組時,空格作為一個字符占有一個字節的空間,我們可以把它看著一個字符去處理。而“\0 ” 則是這個字符串的結束標識符,也占有一個字節的空間,表…

C++11 新特性:Lambda 表達式

參考文章:https://blogs.oracle.com/pcarlini/entry/c_1x_tidbits_lambda_expressions 或許,Lambda 表達式算得上是 C 11 新增特性中最激動人心的一個。這個全新的特性聽起來很深奧,但卻是很多其他語言早已提供(比如 C#&#xff0…

山東自考c語言程序設計停考了嗎,2018山東自考停考專業有哪些

自考每年都會停考一批的專業以適應社會的發展,今年山東自考的停考專業有哪些?本文由學梯網小編整理發布,僅供參考。2018年山東自考停考專業有什么根據山東省教育考試院發布的《關于山東自學考試停考國際貿易(專科)等19個專業的通知》知悉&…

公開調用私有Java方法?

我們是Java開發人員,在Java中已知4種訪問修飾符:私有,受保護,公共和包。 好吧,除了私有外,最后三個可以通過繼承,相同的包或實例從類外部調用。 現在,常見的問題是,可以公…

Java 異常處理 try catch finally throws throw 的使用和解讀(一)

最近的一個內部表決系統開發過程中,發現對異常處理還存在一些模棱兩可的地方,所以想著整理一下主要涉及到:1.try catch finally throws throw 的使用和解讀2.自定義異常的使用3.常見的運行異常 /** * Java 異常處理 * try catch finally thr…

C語言字符像素,返回字符串寬度 (以像素為單位)

[c]代碼庫#include #include #include #include int main(void){/* request auto detection */int gdriver DETECT, gmode, errorcode;int x 0, y 0;int i;char msg[80];/* initialize graphics and local variables */initgraph(&gdriver, &gmode, "");/…

Spring動態物業管理

靜態和動態屬性對于運營管理以及在生產級別更改系統行為都非常重要。 特別地,動態參數減少了服務中斷。 本文展示了如何使用Quartz在Spring Applications中管理動態屬性。 有關使用 Spring和Quartz集成提供“ 使用Spring和Quartz的多作業計劃服務”的文章。 讓我們看…

[BZOJ1005][HNOI2008]明明的煩惱

[BZOJ1005][HNOI2008]明明的煩惱 試題描述 自從明明學了樹的結構,就對奇怪的樹產生了興趣......給出標號為1到N的點,以及某些點最終的度數,允許在任意兩點間連線,可產生多少棵度數滿足要求的樹? 輸入 第一行為N(0 < N < 1000),接下來N行,第i1行給出第i個節點的度數Di,如…

android調用so封裝jni,GitHub - Michelle0716/AndroidJniDemo1: 安卓對c進行so文件打包,并以jni的形式進行調用...

AndroidJniDemo1安卓對c進行so文件打包&#xff0c;并以jni的形式進行調用項目中的部分app &#xff1a; 編譯so文件jniDemo: 添加運行so文件開發環境&#xff1a;android studio 3.0.1(As3.0以上創建項目&#xff0c;選擇支持c/c,項目會自動生成需要的配置&#xff0c;不需要在…

ADF任務流:頁面片段的托管bean范圍

介紹 當我們使用ADF任務流并需要實現一些特定于流的業務邏輯或存儲一些與該流相關的信息時&#xff0c;我們通常使用pageFlowScope托管bean。 而且&#xff0c;當我們需要為流的活動&#xff08;頁面或頁面片段&#xff09;提供服務時&#xff0c;我們將較短的作用域用于此類托…

Linux平臺下:塊設備、裸設備、ASMlib、Udev相關關系

對磁盤設備&#xff08;裸分區&#xff09;的訪問方式分為兩種&#xff1a;1.字符方式訪問&#xff08;裸設備&#xff09;&#xff1b;2.塊方式訪問 Solaris平臺 : 在Solaris平臺下&#xff0c;系統同時提供對磁盤設備的字符、塊方式訪問。每個磁盤有兩個設備文件名: 一個在/d…

Error0---local variable S is accessed from within inner class; needs to be declared final

local variable S is accessed from within inner class; needs to be declared final在內部類當中不能引用本地變量s,需要被聲明為常量轉載于:https://www.cnblogs.com/Cherrylalala/p/6636642.html

android版本如何修改時間,如何修改Android系統默認時間

相信很多人看到過Android手機或平板顯示XXXX-01-01這個奇怪的日期&#xff0c;沒錯&#xff0c;這就是Android設備的默認日期。當Android設備沒有聯網&#xff0c;無法獲取當前真實時間的時候&#xff0c;就會使用系統默認時間。大部分時候&#xff0c;系統默認時間是Epoch時間…

Java Web應用程序的反跨站點腳本(XSS)過濾器

這是為Java Web應用程序編寫的一個好簡單的反跨站點腳本&#xff08;XSS&#xff09;過濾器。 它的主要作用是從請求參數中刪除所有可疑字符串&#xff0c;然后將其返回給應用程序。 這是我以前關于該主題的帖子的改進。 您應該將其配置為鏈&#xff08;web.xml&#xff09;中的…

生成建表腳本up_CreateTable

已經很久沒用使用這個腳本了&#xff0c;今天用到&#xff0c;并做修改&#xff0c;增加了生成擴展屬性功能。 Go if object_ID([up_CreateTable]) is not nullDrop Procedure [up_CreateTable] Go /* 生成建表腳本(V4.0) Andy 2017-3-28 */ Create Proc up_CreateTable (obje…

android程序員周記,程序員實習周記100篇

程序員實習周記100篇有效防止雷同&#xff01;簡單修改即可使用&#xff01;姓名&#xff1a;XXX學號&#xff1a;20190920008專業&#xff1a;M]指導老師&#xff1a;實習時間&#xff1a;20XX-XX-XX—20XX-XX-XX2019年XX月XX日t8in6Ay8Cw7c HuktN6ttTE12V7A eZu9g e7W1Y Dxqx…

Python之裝飾器

裝飾器功能有兩點&#xff1a;1.首先自動執行裝飾器后面跟的這個函數&#xff0c;并將裝飾器修飾的那個函數名作為參數帶入裝飾器后面函數&#xff1b;2.將裝飾器后面函數的返回值&#xff0c;賦值給裝飾器所修飾的那個函數。舉個例子說明&#xff1a; 1 def outer(func):2 …

在獨立Java應用程序中使用Tomcat JDBC連接池

這是從我們的客人文章W4G伙伴克拉倫斯豪的作者臨春3從A按。 您可能會在文章結尾找到本書的折扣券代碼&#xff0c;僅適用于Java Code Geeks的讀者&#xff01; 請享用&#xff01; 在需要數據訪問權限的獨立Java應用程序中使用JDBC連接池時&#xff0c;大多數開發人員將使用com…

Python之路【目錄】 2

http://www.cnblogs.com/wupeiqi/articles/4938499.html轉載于:https://www.cnblogs.com/cp-miao/p/5750211.html