天天看點

【騰訊Bugly幹貨分享】微信Tinker的一切都在這裡,包括源碼(一)

最近半年以來,Android熱更新檔技術熱潮繼續爆發,各大公司相繼推出自己的開源架構。Tinker在最近也順利完成了公司的稽核,并非常榮幸的成為github.com/Tencent上第一個正式公開的項目。

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

回顧這半年多的曆程,這是一條跪着走完,坑坑不息之路。或許隻有自己真正經曆過,深入研究過, 才會真正的明白

熱更新檔不是請客吃飯

對熱更新檔技術本身,還是對使用者來說都是如此。它并不簡單,也有着自己的局限性,在使用之前我們需要對它有所了解。我希望通過分享微信在這曆程中的思考與經驗,能幫助大家更容易的決定是否在自己的項目中使用熱更新檔技術,以及選擇什麼樣方案。

熱更新檔技術背景

熱更新檔是什麼以及它的應用場景介紹,大家可以參考文章微信Android熱更新檔實踐演進之路。

在筆者看來Android熱更新檔技術應該分為以下兩個流派:

  • Native,代表有阿裡的Dexposed、AndFix與騰訊的内部方案KKFix;
  • Java, 代表有Qzone的超級更新檔、大衆點評的nuwa、百度金融的rocooFix, 餓了麼的amigo以及美團的robust。

Native流派與Java流派都有着自己的優缺點,它們具體差異大家可參考上文。事實上從來都沒有最好的方案,隻有最适合自己的。

對于微信來說,我們希望得到一個“高可用”的更新檔架構,它應該滿足以下幾個條件:

  1. 穩定性與相容性;微信需要在數億台裝置上運作,即使更新檔架構帶來1%的異常,也将影響到數萬使用者。保證更新檔架構的穩定性與相容性是我們的第一要務;
  2. 性能;微信對性能要求也非常苛刻,首先更新檔架構不能影響應用的性能,這裡基于大部分情況下使用者不會使用到更新檔。其次更新檔包應該盡量少,這關系到使用者流量與更新檔的成功率問題;
  3. 易用性;在解決完以上兩個核心問題的前提下,我們希望更新檔架構簡單易用,并且可以全面支援,甚至可以做到功能釋出級别。

在“高可用”這個大前提下,微信對當時存在的兩個方案做了大量的研究:

  1. Dexposed/AndFix;最大挑戰在于穩定性與相容性,而且native異常排查難度更高。另一方面,由于無法增加變量與類等限制,無法做到功能釋出級别;
  2. Qzone;最大挑戰在于性能,即Dalvik平台存在插樁導緻的性能損耗,Art平台由于位址偏移問題導緻更新檔包可能過大的問題;

在2016年3月,微信為了追尋“高可用”這個目标,決定嘗試搭建自己的更新檔架構——Tinker。Tinker架構的演繹并不是一蹴而就,它大緻分為三個階段,每一階段需要解決的核心問題并不相同。而Tinker v1.0的核心問題是實作符合性能要求的Dex更新檔架構。

Tinker v1.0-性能極緻追求之路

為了穩定性與相容性,微信選擇了Java流派。目前最大難點在于如何突破Qzone方案的性能問題,這時通過研究Instant Run的冷插拔與buck的exopackage給了我們靈感。它們的思想都是全量替換新的Dex。

簡單來說,我們通過完全使用了新的Dex,那樣既不出現Art位址錯亂的問題,在Dalvik也無須插樁。當然考慮到更新檔包的體積,我們不能直接将新的Dex放在裡面。但我們可以将新舊兩個Dex的差異放到更新檔包中,這裡我們可以調研的方法有以下幾個:

  1. BsDiff;它格式無關,但對Dex效果不是特别好,而且非常不穩定。目前微信對于so與部分資源,依然使用bsdiff算法;
  2. DexMerge;它主要問題在于合成時記憶體占用過大,一個12M的dex,峰值記憶體可能達到70多M;
  3. DexDiff;通過深入Dex格式,實作一套diff差異小,記憶體占用少以及支援增删改的算法。

如何選擇?在“高可用”的核心訴求下,性能問題也尤為重要。非常慶幸微信在當時那個節點堅決的選擇了自研DexDiff算法,這過程雖然有苦有淚,但也正是有它,才有現在的Tinker。

一. DexDiff技術實踐

在不斷的深入研究Dex格式後,我們發現自己跳進了一個深坑,主要難點有以下三個:

  1. Dex格式複雜;Dex大緻分為像StringID,TypeID這些Index區域以及使用Offset的Data區域。它們有大量的互相引用,一個小小的改變可能導緻大量的Index與Offset變化;
  2. dex2opt與dex2oat校驗;在這兩個過程系統會做例如四位元組對齊,部分元素排序等校驗,例如StringID按照内容的Unicode排序,TypeID按照StringID排序…
  3. 低記憶體,快速;這要求我們對Dex每一塊做到一次讀寫,無法像baksmali與dexmerge那樣完全結構化。

這不僅要求我們需要研究透Dex的格式,也要把dex2opt與dex2oat的代碼全部研究透。現在回想起來,這的确是一條跪着走完的路。與研究Dalvik與Art執行一緻,這是經曆一次次翻看源碼,一次次編Rom檢視日志,一次次dump記憶體結構換來的結果。

下面以最簡單的Index區域舉例:

要想将從左邊序列更改成右邊序列,Diff算法的核心在于如何生成最小操作序列,同時修正Index與Offset,實作增删改的功能。

  1. Del 2;”b”元素被删除,它對應的Index是2,為了減少更新檔包體積,除了新增的元素其他一律隻存Index;
  2. “c”, “d”, “e”元素自動前移,無須操作;
  3. Addf(5); 在第五個位置增加”f”這個元素。

對于Offset區,由于每個Section可能有非常多的元素,這裡會更加複雜。最後我們得到最終的操作隊列,為什麼DexDiff可以做到記憶體非常少?這是因為DexDiff算法是每一個操作的處理,它無需一次性讀入所有的資料。DexDiff的各項資料如下:

通過DexDiff算法的實作,我們既解決了Dalvik平台的性能損耗問題,又解決了Art平台更新檔包過大的問題。但這套方案的缺點在于占Rom體積比較大,微信考慮到移動裝置的存儲空間提升比較快,增加幾十M的Rom空間這個代價可以接受。

二. Android N的挑戰

信心滿滿上線後,卻很快收到華為回報的一個Crash:

而且這個Crash隻在Android N上出現,在當時對我們震動非常大,難道Android N不支援Java方式熱更新檔了?難道這兩個月的辛苦都白費了嗎?一切想象都蒼白無力,隻有繼續去源碼裡面找原因。

在之前的基礎上,這一塊的研究并沒有花太多的時間,主要是Android N的混合編譯模式導緻。更多的詳細分析可參考文章Android N混合編譯與對熱更新檔影響解析。

三. 廠商OTA的挑戰

剛剛解決完Android N的問題,還在沉醉在自己的勝利的愉悅中。前線很快又傳來噩耗,小米回報開發版的一些使用者在微信啟動時黑屏,甚至ANR.

當時第一反應是不可能,所有的DexOpt操作都是放到單獨的程序,為什麼隻在Art平台出現?為什麼小米開發版使用者回報比較多?經過分析,我們發現優化後odex檔案存在有效性的檢查:

  • Dalvik平台:modtime/crc…
  • Art平台: checksum/image_checksum/image_offset…

這就非常好了解了,因為OTA之後系統image改變了,odex檔案用到image的偏移位址很可能已經錯誤。對于ClassN.dex檔案,在OTA更新系統已完成重新dex2oat,而更新檔是動态加載的,隻能在第一次執行時同步執行。

這個耗時可能高達十幾秒,黑屏甚至ANR也是非常好了解。那為什麼隻有小米使用者回報比較多呢?這也是因為小米開發版每周都會推送系統更新的原因。

在當時那個節點上,我們重新的審視了全量合成這一思路,再次對方案原理本身産生懷疑,它在Art平台上面帶來了以下幾個代價:

  1. OTA後黑屏問題;這裡或許可以通過lLoading界面實作,但并不是很好的方案;
  2. Rom體積問題;一個10M的Dex,在Dalvik下odex産物隻有11M左右,但在Art平台,可以達到30多M;
  3. Android N的問題;Android N在混合編譯上努力,被更新檔全量合成機制所廢棄了。這是因為動态加載的Dex,依然是全量編譯。

回想起來,Qzone方案它隻把需要的類打包成更新檔推送,在Art平台上可能導緻更新檔很大,但它肯定比全量合成10M的Dex少很多很多。在此我們提出分平台合成的想法,即在Dalvik平台合成全量Dex,在Art平台合成需要的Dex

DexDiff算法已經非常複雜,事實上要實作分平台合成并不容易。

主要難點有以下幾個方面:

  • small dex的類收集;什麼類應該放在這個小的Dex中呢?
  • ClassN處理;對于ClassN怎麼樣處理,可能出現類從一個Dex移動到另外一個Dex?
  • 偏移二次修正; 更新檔包中的操作序列如何二次修正?
  • Art.info的大小; 為了修正偏移所引入的info檔案的大小?

慶幸的是,面對困難我們并沒有畏懼,最後實作了這一套方案,這也是其他全量合成方案所不能做到的:

  1. Dalvik全量合成,解決了插樁帶來的性能損耗;
  2. Art平台合成small dex,解決了全量合成方案占用Rom體積大, OTA更新以及Android N的問題;
  3. 大部分情況下Art.info僅僅1-20K, 解決由于更新檔包可能過大的問題;

事實上,DexDiff算法變的如此複雜,怎麼樣保證它的正确性呢?微信為此做了以下三件事情:

  1. 随機組成Dex校驗,覆寫大部分case;
  2. 微信200個版本的随機Diff校驗, 覆寫日常使用情況;
  3. Dex檔案合成産物有效性校驗,即使算法出現問題,也隻是編譯不出更新檔包。

每一次DexDiff算法的更新,都需要經過以上三個Test才可以送出,這樣DexDiff的這套算法已完成了整個閉環。

四. 其他技術挑戰

在實作過程,我們還發現其他的一些問題:

  1. Xposed等微信插件; 市面上有各種各樣的微信插件,它們在微信啟動前會提前加載微信中的類,這會導緻兩個問題:

    a. Dalvik平台:出現Class ref in pre-verified class resolved to unexpected implementation的crash;

    b. Art平台:出現部分類使用了舊的代碼,這可能導緻更新檔無效,或者位址錯亂的問題。

    微信在這裡的處理方式是若crash時發現安裝了Xposed,即清除并不再應用更新檔。

    1. Dex反射成功但是不生效;部分三星android-19版本存在Dex反射成功,但出現類重複時,查找順序始終從base.apk開始。

      微信在這裡的處理方式是增加Dex反射成功校驗,具體通過在架構中埋入某個類的isPatch變量為false。在更新檔時,我們自動将這個變量改為true。通過這個變量最終的數值,我們可以知道反射成功與否。

Tinker v1.0總結

一. 關于性能

通過Tinker v1,0的努力,我們解決了Qzone方案的性能問題,得到一個符合“高可用”性能要求的更新檔架構。

  • 它更新檔包大小非常少,通常都是10k以内;
  • 對性能幾乎沒有影響, 2%的性能影響主要原因是微信運作時校驗更新檔Dex檔案的md5導緻(雖然檔案在/data/data/目錄,微信為了更進階别的安全);
  • Art平台通過革命性的分平台合成,既解決了位址偏移的問題,占Rom體積與Qzone方案一緻。

二. 關于成功率

也許有人會質疑微信成功率為什麼這麼低,其他方案都是99%以上。事實上,我們的成功率計算方式是:

應用成功率= 更新檔版本轉化人數/基準版本安裝人數

即三天後,94.1%的基礎版本都成功更新到更新檔版本,由于基礎版本人數也是持續增長,同時可能存在基準或更新檔版本使用者安裝了其他版本,是以本統計結果應略為偏低,但它能現實的反應更新檔的線上總體覆寫情況。

事實上,采用Qzone方案,3天的成功率大約為96.3%,這裡還是有很多的優化空間。

三. Tinker v2.0-穩定性的探尋之路

在v1.0階段,大部分的異常都是通過廠商回報而來,Tinker并沒有解決“高可用”下最核心的穩定性與相容性問題。我們需要建立完整的監控與更新檔回退機制,監控每一個階段的異常情況。這也是Tinker v2.0的核心任務,由于邊幅問題這部分内容将放在下一篇文章。

關注Tinker,來Github給我們star吧

https://github.com/Tencent/tinker

更多精彩内容歡迎關注bugly的微信公衆賬号:

騰訊 Bugly是一款專為移動開發者打造的品質監控工具,幫助開發者快速,便捷的定位線上應用崩潰的情況以及解決方案。智能合并功能幫助開發同學把每天上報的數千條 Crash 根據根因合并分類,每日日報會列出影響使用者數最多的崩潰,精準定位功能幫助開發同學定位到出問題的代碼行,實時上報可以在釋出後快速的了解應用的品質情況,适配最新的 iOS, Android 官方作業系統,鵝廠的工程師都在使用,快來加入我們吧!