【騰訊Bugly乾貨分享】微信熱補丁Tinker的實踐演進之路

本文來自於騰訊bugly開發者社區,非經做者贊成,請勿轉載,原文地址:http://dev.qq.com/topic/57ad7a70eaed47bb2699e68egit

Dev Club 是一個交流移動開發技術,結交朋友,擴展人脈的社羣,成員都是通過審覈的移動開發工程師。每週都會舉行嘉賓分享,話題討論等活動。github

本期,咱們邀請了騰訊WXG Android開發工程師——張紹文,爲你們分享**《微信熱補丁Tinker的實踐演進之路》**。算法


分享內容簡介: Tinker 是微信官方的 Android 熱補丁解決方案,它支持動態下發代碼、So庫以及資源,讓應用可以在不須要從新安裝的狀況下實現更新。這裏大體介紹 Tinker 的實現原理,當時遇到的各類坑以及對它各個方面性能的優化工做。安全

內容大致框架:性能優化

  1. 當前各類熱補丁框架的比較以及 Tinker 的設計目標
  2. Tinker的實踐演進
  3. Tinker在實現中遇到的困難

下面是本期分享內容整理微信


hello,你們好。我是張紹文,目前在微信主要負責 Android 的性能優化以及終端質量平臺相關工做。app

下面開始咱們今天的分享。框架

1. 當前各類熱補丁框架對比 & Tinker 的設計目標

熱補丁技術是當前很是熱門的 Android 開發技術,其中比較出名的方案有支付寶的 AndFix以及** QZone 的超級熱補丁**方案。ide

微信大約在2015年6月開始嘗試應用,通過研究與嘗試現有的各個方案,咱們發現它們都有着自身的一些侷限性。咱們最終採用不一樣於它們的技術方案,自研微信熱補丁開源框架 Tinker。性能

下面咱們先來說講先有框架的一些侷限性。

1.1 AndFix

Andfix 是阿里推出的開源框架,它在 github 的地址是:

https://github.com/alibaba/AndFix

它的技術原理以下圖:它採用 native hook 的方式,這套方案直接使用 dalvik_replaceMethod 替換 class 中方法的實現。

它的缺點主要包括如下幾個:

  1. **兼容性不佳;**因爲它採用 native 替換的方式,在 github Issue 中也有大量崩潰的反饋;

  2. **成功率不高;**不支持修改 inline 方法,不支持修改方法參數超過8個或參數中帶有 long, double 或者 float。跟一些使用 Andfix 的產品討論過,它們的成功率不超過40%;

**緣由:**只替換了 DexCache 中的 ArtMethod 結構體,對於 Art 中一些 compiledCode 是直接經過 bx 過去

  1. **開發不透明;**因爲它還不支持增長 filed,咱們須要爲了補丁而補丁,沒法採用這個技術發佈需求。

Andfix 的好處是能夠馬上生效,但它能夠支持的補丁場景很是有限,僅僅可使用它來修復特定問題。

因此咱們不考慮採用這個方案。

1.2 Qzone 超級補丁方案

如今咱們講講 Qzone 超級補丁方案,在騰訊內部已開源。

這個方案使用 classloader 的方式,能實現更加友好的類替換。並且這與咱們加載 Multidex 的作法類似,能基本保證穩定性與兼容性。

它主要的面臨問題有兩個:

  1. 爲了解決 unexpected DEX problem 異常,而採用插樁的方式給全部類插入不會真正運行的代碼,防止類打上 preverify 標誌。 採用插樁致使全部類都非 preverify,致使上圖中的 verify 與 optimize 操做會在加載類時觸發。這會有必定的性能損耗,微信分別採用插樁與不插樁兩種方式作過兩種測試,一是連續加載700個50行左右的類,一是統計微信整個啓動完成的耗時。

  2. 在 art 平臺,若補丁中的類出現 Field、Method 或 Interface 變化,可能會致使出現內存地址錯亂的問題。爲了解決這個問題,咱們最後補丁中的類要有如下規則: **a. **修改跟新增的 class; **b. **若 class 有 field,method 或 interface 數量變化,它們全部的子類; **c. **若 class 有 field,method 或 interface 數量變化,它們以及它們全部子類的調用類。若是採用 ClassN 方式,即須要多個 dex 一塊兒處理。

Qzone 的方案最爲簡單,並且開發透明,補丁的成功率也是很是高的。

但因爲微信對於運行性能以及補丁大小都比較敏感,咱們最終也沒有采用這套方案。

1.3 Tinker 的設計目標

那麼微信但願的是一套怎麼樣的熱補丁框架呢,咱們認爲主要的目標有如下幾個:

  1. **開發透明;**開發者無需關心是否在補丁版本,他能夠隨意修改,不禁框架限制;
  2. **性能無影響;**補丁框架不能對應用帶來性能損耗;
  3. **完整支持;**支持代碼,So 庫以及資源的修復,能夠發佈功能。
  4. **補丁大小較小;**補丁大小應該儘可能的小,提升升級率。
  5. **穩定,兼容性好;**保證微信的數億用戶的使用,儘可能減小反射;

2. 3. Tinker 的實踐演進

如今咱們來說講微信熱補丁框架 Tinker 的實現,目前在騰訊內部已開源。

它的名字來至 Dota 中的地精修補匠,咱們但願發版本能夠像它同樣作到無限刷新。

Tinker 的方案來源 gradle 編譯的 instant run 與 buck 編譯的 exopackage。它們的思想都是全量替換新的 Dex。即咱們徹底使用了新的 Dex,那樣既不出現 Art 地址錯亂的問題,在 Dalvik 也無須插樁。

可是 instant run 針對的是編譯期,它能夠直接將最後生成的全部變化都直接拷到手機端。對於線上方案,這確定是不可行的。因此當前核心問題是找到合適的,使補丁結果更小的差分算法。

微信首先 demo 中採用的是 bsdiff,它無關文件格式,但對於dex效果不是特別好,並且很是不穩定。當前微信對於 so,依然使用 bsdiff 算法。

而後咱們想到 dexmerge 算法,把修改跟新增的類經過 dexmerge 方式與原來的 dex 合併,從而獲得最終的完整 Dex。

通過實踐,dexmerge 的核心問題有兩個:

  1. **沒法刪除 class;**致使在 Dalvik 平臺會出現加載類重複的狀況,這要求咱們只能採用 miniloader 加載方案來避免;
  2. **合成時內存佔用過大;**dexmerge 庫使用場景在 pc,它沒有太多的考慮內存問題。它的峯值內存能夠達到輸入 dex 的大小的4倍-6倍。一個12M的 dex,峯值內存可能達到70多M。

最後咱們決定基於 dex 的格式,自研出一種 Dexdiff 算法,它須要達到如下目標;

  1. diff 結果小;
  2. 合成過程佔用內存小;
  3. 支持刪除、新增、修改 dex 中的 class。

這裏面主要的原理是深度利用原來 dex 中的信息,對於 dex 的每個 section 作處理。這塊在今天再也不深刻,感興趣的同窗能夠交流。

內存方面 dexdiff 峯值內存是 dex 的兩倍左右,達到預期的結果。

對於微信熱補丁的更多信息,能夠閱讀我以前發的一篇文章。

微信Android熱補丁實踐演進之路

而後咱們來看看 Tinker 的框架設計,它主要包括如下幾部分:

  1. **補丁合成;**這些都在單獨的 patch 進程工做,這裏包括 dex,so 還有資源,主要完成補丁包的合成以及升級;
  2. **補丁的加載;**若是經過反射系統加載咱們合成好的 dex,so 與資源;
  3. **監控回調;**在合成與加載過程當中,出現問題及時回調;
  4. 版本管理; Tinker 支持補丁升級,甚至是多個補丁不停的切換。這裏咱們須要保證全部進程版本的一致性;
  5. **安全校驗;**不管在補丁合成仍是加載,咱們都須要有必要的安全校驗。

在微信中,咱們爲 Tinker 框架加入了100多個實時上報,監控着在每一個過程可能出現的問題:

3.Tinker 在實現中遇到的困難

接下來咱們來看看在開發 Tinker 過程當中,遇到的一些問題:

1. 廠商 OTA;

對於 Art 平臺,dex2oat 時間較長。特別是廠商 OTA 以後,全部動態加載的代碼都須要從新執行 dex2oat。這是由於 boot image 已經改變,可是系統在升級時只會給 ClassN.dex 從新 OTA。

對於補丁 dex 會出現主進程同步執行 dex2oat,這個時間很是久,頗有可能會出現 ANR,對於小米等一些產品的開發板更是如此。這也是咱們如今努力在實現分平臺合成的緣由,即在 Art平臺,只合成規則下須要的 class。只要不是全量替換,從新 dex2oat 的時間是能夠接受的。

2. Android N 混合編譯致使補丁機制失效

這塊花了必定的時間從新梳理了 Android N art 的代碼,詳細的分析能夠查看以前我發的一篇文章。

Android N混合編譯與對熱補丁影響解析

3. Dex 反射成功可是不生效;

開始的時候,咱們加載補丁 dex 採用的是 makedexElement 的方式。可是發現大約有幾十萬臺機器,補丁加載成功了,可是使用的仍是舊版本的代碼。某些機器相似三星 s6 502系統,儘管反射 pathList 成功,查找順序依然以 base.apk 優先。

這裏採起的解決方法是相似 instant run,採用反射 parent classloader 的方式。這裏不得不提,instant run 的 increaseClassLoader 實現很是精妙。

4. Xposed 等微信插件;

市面上有各類各樣的微信插件,它們在微信啓動前會提早加載微信中的類,這會致使兩個問題:

**a. **在 Dalvik 平臺,直接出現 Class ref in pre-verified class resolved to unexpected implementation 的 crash;

**b. **在 Art 平臺,因爲出現部分類使用了舊的代碼,這可能致使補丁無效,或者地址錯亂的問題。

它們根本的緣由都是Xposed反射調用,提早導入了咱們的某些類。

事實上,因爲補丁使用不當或者其餘問題,咱們的確須要有一個安全模式。即在應用啓動不起來或屢次 crash 時,進入補丁清理或者升級的流程。

結語

也許有人以爲 Tinker 過於臃腫,過於複雜。這是由於熱補丁並非僅僅加載一個 dex 或 so 文件,事實上它要關心的細節有不少。進程的一致性,控制可修改類的範圍,版本的管理,擴展性等等。

Tinker 的將來規劃是真正的開源出去,大約下週會提交分享平臺合成以及資源相關的全部代碼。而後等公司的開源審計結束後將在 github 開源,歡迎你們接入 Tinker 內測,給咱們更多的意見。

因爲時間有限,今天的分享就到這裏。對於 So,資源的合成方式,dexdiff 的技術細節,若你們感興趣能夠與咱們交流。

互動問答環節

**Q1:**請教下 patch 進程和主進程是怎麼通訊的?

是經過 intent service 通訊的,主進程一個接受補丁結果的 intent service,patch 進程是一個接受補丁請求的 intent service

Q2:「分平臺合成」沒聽太明白,能再仔細說下麼?

分平臺合成就是在 Dalvik 平臺,咱們合成全量的 dex,這能夠避免咱們插樁的要求。

在 Art 平臺,咱們只合成上述三個條件下的類:

a. 修改跟新增的 class; b. 若 class 有 field,method 或 interface 數量變化,它們全部的子類; c. 若 class 有 field,method 或 interface 數量變化,它們以及它們全部子類的調用類。若是採用 ClassN 方式,即須要多個 dex 一塊兒處理。

這裏的難點是同一份 diff 代碼,能夠作到不一樣的合成方式。

**Q3:**對於內部空間不足引發的 patch 失敗如今有什麼好的解決辦法?

對於咱們的方案,空間佔用有可能比較大,咱們解決的方法有兩個:

  1. 在 patch 以前提早檢查用戶的剩餘空間,若是用戶剩餘空間過少,即不嘗試。
  2. 若本次失敗,咱們會有回調,而後咱們會按期重試三次。

你也能夠在這裏採用提示用戶清理空間。Tinker 框架是能夠高度定製化的。

**Q4:**對於替換 classloader 失敗後再用 MultiDex install 這種方案有什麼考慮?

有的,對於替換失敗的話,的確會回退到相似 Multidex install 方式的

**Q5:**目前微信對熱補丁技術的應用場景通常集中在哪些方面呢?除了修復緊急的 BUG,還有哪些真實場景下用過這個技術嗎?微信是如何評估是否須要經過打熱補丁的方式來處理一些問題的呢?

正如我以前的一篇文章來講,在 Android 熱補丁技術的應用比 iOS 更加容易。咱們能夠徹底作到無感知的開發,推給用戶等。這裏面的應用場景有不少,用戶調試,版本升級,發佈需求,Abtest 等等。

**Q6:**想問下大神,對於替換 app 中使用的第三方 jar 包,有具體實踐嗎?

抱歉,這部分尚未實踐。原理上是沒問題的,若是第三方的 jar 包是集成到源碼,那麼編譯新包的時候已經能夠帶上改變。若是第三方的 jar 包是動態加載的,也是沒有問題的。咱們經過 parent classloader 的方式,查找順序也會在大家以前。

**Q7:**patchCoreSDK 怎麼繞過 換 classloader 後跨 dex 加載類 accesserror 的問題?有對 patchcoreSDK 作強制訪問隔離嗎?

是的,Tinker 框架分爲兩部分,核心加載代碼,成爲 loader 類,這裏大概有十幾個類,他們是不容許修改的。其餘大部分 Tinker 的類也是能夠經過補丁修改的,這裏 Tinker 框架已經作了處理,即在新合成的 Dex,咱們已經刪除了 loader 相關的類,從而完全避免了這個問題。

**Q8:**patch 成功後怎麼及時重啓其餘進程?

爲了保證各個進程的惟一性,咱們有一個版本管理文件用於記錄當前補丁的版本。它分爲 old 與 new 兩個字段。同時作了約定,只有 patch 進程能夠修改 new 字段,只有主進程能夠修改 old 字段,其餘全部進程啓動時都只會加載 old 字段的補丁版本。而後主要主進程能夠發起版本升級,即把 new 字段賦值給 old 字段,這個時候主進程要殺掉其餘全部的進程,以保證統一性。 而及時重啓其餘進程的問題,主要是在我剛纔講的 result service。在結果回調中,咱們若是發現補丁已經成功了,咱們能夠設置主進程在後臺或者鎖屏時自殺,以達到最快的應用。

**Q9:**徹底使用新的資源包是怎麼理解?舊的資源包會被替換刪除嗎?

舊的資源包是安裝的 apk,咱們是不會刪掉的。咱們只是反射系統的一些接口,把它替換成新的資源包

**Q10:**超級補丁方案,有沒有想過不採用插樁的方式,而是去 hook 檢驗的方法,就能緩解性能的問題?

事實上,有些人實現 hook preverify 標誌來避免插樁。可是看過底層代碼,就知道是不可行的。咱們要知道系統檢查那個標誌位的真正緣由,即便 hook 了 preverify 標誌,在真正運行過程當中,因爲 quck 指令以及 vtable 的優化,依然運行時會出問題。這個問題告訴咱們,作事情須要知其然也要知其因此然。

**Q11:**合成新的資源和 so 是怎麼加載的?

so 能夠經過反射 classloader 的 lib path,可是咱們並不建議這麼多,一來是兼容性問題,二來在某些機器上,多 abi 的判斷並不許確。咱們更但願經過封裝代碼來支持。對於資源,咱們處理是跟 dex 差很少,啓動時即反射調用。

**Q12:**是否有動態下發第三方的 jar 包,如何調用第三方 jar 包的方法。反射?

Tinker 框架只會合成輸入 pattern 下的 dex,並且在啓動的時候把他們加載。若是調用的問題,使用者本身決定的。

**Q13:**差量下發更新,合成的時候是否會有性能問題?是否支持(圖片)資源的差量下發?

合成的話,咱們對於內存、GC 以及耗時都有大量的優化。即便是微信這樣體量的 app,從外部監控來看,大部分用戶都能在60秒之內完成。

**Q14:**須要在補丁合成加載以後才進入程序(交由用戶操做)嗎?

合成與加載是分開兩個過程,咱們的原則是除非合成已經完全完成,否則其餘進程是不會去加載的。即補丁不會去影相其餘進程的加載性能

**Q15:**代碼徹底開源嗎?

對的,全部代碼都會開源,從編譯到各個模塊。

**Q16:**xposed 框架的那些插件,是經過反射調用替換值?那通常有啥方式保證安全性?保證 app 數據的安全性?

它們只要是反射調用微信的某些類,達到某些功能的篡改。事實上,若是在 root 下,單純的保護是比較難的。

**Q17:**爲何要在補丁成功的時候加結果回調是爲了啓動程序麼,可是和您剛纔說的爲了實時上報?

回調結果是爲了給使用者一個回調,在這個回調裏面它能夠作各類各樣的工做。例如我彈出升級完成的 dialog。我設置鎖屏或者程序進入後臺後自殺,這能夠加快補丁的應用

**Q18:**既然能加載 so 和資源,Tinker 能用於插件化嗎?

Tinker 當前沒有作四大組件的代理,可是 Tinker 將來絕對是具有這個能力的

**Q19:**merge 失敗後的補救機制是怎樣的?能夠回退麼?

merge 失敗,咱們會收到回調,這個時候咱們不會加載的。在默認實現裏面,咱們會刪除這些臨時文件。

**Q20:**這套框架目前是多少我的在維護呢?

Tinker當前有3我的在開發維護

**Q21:**請問資源是編譯到 arsc 中仍是反射加載二進制流?

你的問題我不太明白,資源咱們採用的是全量替換,即徹底使用新的資源包

**Q22:**在加入 Tinker 以後,對各平臺的加固適配如何?微信是否有加固?

微信沒有使用加固,可是加固應該是不影響的,只須要把接口改一下就能夠了。

相關文章
相關標籤/搜索