天天看點

QFix 探索之路 —— 手Q熱更新檔輕量級方案

本文為騰訊 Bugly 開發者社群投稿,作者:葉哲恺,非經作者同意,請勿轉載。

原文位址:http://dev.qq.com/topic/57ff5832bb8fec206ce2185d

導語

QFix 是手Q團隊近期推出的一種新的 Android 熱更新檔方案,在不影響 App 運作時性能(無需插樁去 preverify)的前提下有效地規避了 dalvik 下”unexpected DEX”的異常,而且還是很輕量級的實作:隻需調用一個很簡單的方法就能辦到。

熱更新檔方案及手Q上的使用

自2015年 Android 熱更新檔技術開始出現,之後各種方案和架構層出不窮,原創性的技術方案主要有以下幾種:

QFix 探索之路 —— 手Q熱更新檔輕量級方案

手Q從去年開始研究更新檔方案,當時微信的 Tinker 還沒有推出,考慮到相容性和穩定性,就選用了 Java 反射 hack classloader 的方案,而且和當時已經很成熟的分 dex 從原理上很類似,主要的難點是如何解決 Qzone 發現的 dalvik 下”unexpected DEX”異常,由于沒有研究出其它方法,就沿用了 Qzone 原創的插樁去 preverify 的解決方案,自2016年1月熱更新檔開始在手Q正式版本投入使用,至今解決問題十多個,修複效果十分明顯,穩定性也很好。

性能無法提升,需要改變

插樁的解決方案會影響到運作時性能的原因在于:app 内的所有類都預埋引用一個獨立 dex 的空類,導緻安裝 dexopt 階段的 preverify 失敗,運作時将再次 verify+optimize。近期我們通過 ReDex 嘗試優化手Q的啟動性能時發現:

  • 保留手Q現有的插樁,啟動性能沒有任何優化效果;
  • 去掉插樁,優化手Q啟動相關類的 dex 分布,啟動性能提升 30%。

另外即使後期手Q的釋出版本實際上無需釋出更新檔,我們也需要預埋插樁的邏輯,這本身也是不合理的一點,是以确實有必要去探索新的方向,既保留更新檔的能力,同時去掉插樁帶來的負面影響。

重新分析”unexpected DEX”異常

尋找新的解決方案,還是需要回過頭來分析下這個異常出現的條件:

QFix 探索之路 —— 手Q熱更新檔輕量級方案

這是 dalvik 的一段源碼,當更新檔安裝後,首次使用到更新檔裡的類時會調用到這裡,需要同時滿足圖中标出來的三個條件,才能出現異常,這三個條件的含義如下:

QFix 探索之路 —— 手Q熱更新檔輕量級方案

可以看出,Qzone 的插樁方案是突破了條件2的限制(統一去掉了所有引用類的 preverify 标志),而微信 Tinker 的 dex 增量合成方案是突破了條件3的限制(将更新檔和 app dex 合成後替換,原先 app 裡在同一個 dex 的兩個類,其中一個後來打在更新檔裡,合成後還是會在同一個 dex裡),那有沒有辦法從條件1入手呢?條件1中 fromUnverifiedConstant 為 true 就行,其實之前就有從這個條件進行突破的方案:

http://blog.csdn.net/xwl198937/article/details/49801975

主要思路是:每當系統調用到這個方法,通過 native hook 攔截這個系統方法,更改這個方法的入口參數,将 fromUnverifiedConstant 統一改為 true,但和 Andfix 類似,native hook 方式存在各種相容性和穩定性問題,而且攔截的是一個涉及 dalvik 基礎功能同時調用很頻繁的方法,無疑風險會大很多。

找到新的“大陸”

這段邏輯所在的方法是 dvmResolveClass,通過類之間的引用會調用這個方法,入口參數分别是引用類的 ClassObject,被引用類的 classIdx,以及引用關聯的 dalvik 指令是否為 const-class/instance-of,傳回的是被引用類的 ClassObject,經反複閱讀分析,終于發現了一個可以利用的細節:

QFix 探索之路 —— 手Q熱更新檔輕量級方案
QFix 探索之路 —— 手Q熱更新檔輕量級方案

dvmResolveClass 在最開始會優先從目前 dex 已解析類的緩存裡找被引用類,找到了直接傳回,找不到時說明被引用類還沒有被加載,接着加載成功後,會往目前 dex 緩存裡設定上這個類的引用,後續所有對更新檔類的解析引用都不會走到後面的“unexpected DEX”異常邏輯裡,至于 dex 裡已解析類 get/set 的相關邏輯如下:

QFix 探索之路 —— 手Q熱更新檔輕量級方案
QFix 探索之路 —— 手Q熱更新檔輕量級方案
QFix 探索之路 —— 手Q熱更新檔輕量級方案

結合以上分析,我想到一個思路:隻需首次引用到更新檔類時能夠成功突破上述三個條件之一的限制即可,Qzone 突破條件2和 Tinker 突破條件3的方法操作過重,而且帶來的影響是持續性的,而從條件1入手很簡單:更新檔安裝後,預先以 const-class/instance-of 方式主動引用更新檔類,這次引用會觸發加載更新檔類并将引用放入 dex 的已解析類緩存裡,後續 app 實際業務邏輯引用到更新檔類時,直接從已解析緩存裡就能取到,這樣很簡單地就繞開了“unexpected DEX”異常,而且這裡隻是很簡單地執行了一條輕量級的語句,并沒有其它額外的影響。

另外考慮多 dex 的情況,更新檔類很可能被多個不同 dex 裡的類引用,那麼需要在每個 dex 裡找到一個引用類來預先引用更新檔類嗎?如果 app 裡引用類和更新檔類原本是在同一個 dex 裡,引用類有可能是 preverify 的,這種情況是需要預先引用的;如果原本就不是一個 dex 裡的,引用類由于有對其它 dex 類的依賴,就肯定不是 preverify 的,這種情況條件2本來就是不滿足的,就沒有必要預先引用了,是以可以推斷出隻需要針對更新檔類在原先 App 所對應的 dex 進行預先引用即可。

梳理了思路後,馬上在一個簡單的 demo 上驗證:

QFix 探索之路 —— 手Q熱更新檔輕量級方案

demo 裡更新檔包含的類是 BugObject,通過對比,如果代碼不包含上圖紅框裡的預先引用的邏輯,出現了預期的“unexpected DEX”異常,如果加上這一行代碼,demo 運作正常,而且更新檔的修複功能也生效。通過 dexdump 檢視,确實是優先通過 const-class 指令引用更新檔類的。

QFix 探索之路 —— 手Q熱更新檔輕量級方案

沒那麼簡單,初步方案行不通

上面的 demo 預埋了更新檔裡包含的類,但在實際運用中我們是無法預先設定哪些類要打更新檔的,dex 裡對更新檔類 const-class/instance-of 方式的引用指令是編譯時确定的,但具體是哪些類又需要在運作時動态确定,是以這種動态方式行不通,最初想到的是類似插樁的做法,預先把 app 裡所有類都以 const-class 方式引用一遍,但很明顯有以下問題:

1)由于 App 裡類的數量很多,所有類的預先引用統一放在一個地方肯定不現實,需要分散在多個區,隻對更新檔類所在的少數幾個區執行預先引用的操作,但這裡如何劃分的粒度不好把握,而且 App 裡的類及數量一直變化,我們做過一些嘗試,但沒有比較理想的可考量的方案。

2)預先引用解析所有類,會增加引用類的加載耗時和引用語句本身的執行耗時,對于執行耗時,可以通過添加條件判斷來優化,如果要解析的類在更新檔類名清單裡就執行該語句,否則就不執行,對于加載耗時,初步的測試結果如下(這裡一個劃分的區包含500個左右的類,并進一步區分了是否 preverify,而測試的更新檔包裡包含2個類):

QFix 探索之路 —— 手Q熱更新檔輕量級方案

從測試資料看,加載的耗時較長,而且更新檔類不可預期,如果不巧分布在多個區裡,累計耗時的影響将會嚴重得多。

3)該方案實作起來特别繁瑣,不實用。

确定最終方案

新的方案在 Java 層找不到可行的實作方式,就嘗試從 native 層切入,隻需首次引用解析更新檔類時,直接通過 jni 調用 dalvik 的 dvmResolveClass 這個方法,當然傳入的參數 fromUnverifiedConstant 需要設為 true,這個思路與前面說的 native hook 方式不同,不會去 hook 這個系統方法,而是從 native 層直接調用:

  1. dvmResolveClass 方法是在 dalvik 的系統庫 /system/lib/libdvm.so 裡,通過 dlopen 即可擷取該系統庫的句柄
  2. 通過 dlsym 擷取 dvmResolveClass 這個方法的位址
  3. 設定 dvmResolveClass 這個方法的三個入口參數,再調用 dvmResolveClass:

    1)引用類 referrer 的 ClassObject:這裡需要設定一個引用類,并且能夠擷取到該類的 ClassObject;

    2)更新檔類的 classIdx:需要擷取更新檔類在 app 原先所在 dex 的 classIdx,通過這個 classIdx 可以在 dex 裡找到已解析的類或者擷取類的名字;

    3)布爾值 fromUnverifiedConstant:在C/C++層,這個值可以固定設定為1或者 true。

這裡的關鍵是能擷取到前兩個參數的值,第一個參數引用類的 ClassObject,最初借鑒的是 dvmResolveClass 裡調用的 dvmFindClassNoInit 這個方法,但這個方法擷取一個類的 ClassObject 需要兩個參數,其中類名很容易構造,但需要額外的操作擷取引用類的 ClassLoader 對象的位址,之後又找到一個更便利的方法 dvmFindLoadedClass:

QFix 探索之路 —— 手Q熱更新檔輕量級方案

這個方法隻用傳入類的描述符即可,但必須是已經加載成功的類,在更新檔注入成功後,在每個 dex 裡找一個固定的已經加載成功的引用類并不難。對于主dex,直接用 XXXApplication 類就行,對于其它分 dex,手Q的分 dex 方案有這樣的邏輯:每當一個分 dex 完成注入,手Q都會嘗試加載該 dex 裡的一個固定空類來驗證分 dex 是否注入成功了,是以這個固定的空類可以作為更新檔的引用類使用。第二個參數 classIdx,可以通過 dexdump -h 擷取:

QFix 探索之路 —— 手Q熱更新檔輕量級方案

這個過程可以通過一個小程式自動進行:

輸入: 原有 apk 的所有 dex、更新檔包所有的類名

輸出: 更新檔包每個類所在 dex 的編号以及 classIdx 的值

注1: 如果在更新檔新增原 app 不存在的類,運作時新增類隻會被更新檔 dex 即同一個 dex 裡的類所引用,是以新增的更新檔類無需預先解析引用。

注2: 由于”unexpected DEX”異常出現在 dalvik 的實作裡,art 模式下不會存在,以上預先引用更新檔類的邏輯隻需用在5.0以下的系統。

最終新方案的整體實作流程如下圖所示:

QFix 探索之路 —— 手Q熱更新檔輕量級方案

可以看出,新的方案是很輕量級的實作,隻需一個很簡單的 jni 方法調用就能解決問題,既不用建構時預先插樁去 preverify,也不用下載下傳更新檔後進行 dex 的全量合成。

相容性問題及解決

這個方案由于是 native 層的,我們也通過衆測方式對相容性做了充分的驗證:

1. 不同系統版本導出符号:

在2.x版本dalvik是用C寫的,2.3以上的4.x版本是用C++寫的,基于C++ name mangling原理, dvmFindLoadedClass在編譯後會變為_Z18dvmFindLoadedClassPKc,但經IDA反彙編libdvm.so分析,dvmResolveClass沒有變化

2. yunos ROM的相容性問題:

在第一次衆測任務中,有446位使用者參與,其中有6位回報更新檔不生效的問題,從回報的結果碼看都是libdvm.so加載成功,但是符号導出為NULL導緻的,後來發現這6位使用者安裝的都是yunos的rom,經分析定位到原因如下:

QFix 探索之路 —— 手Q熱更新檔輕量級方案

可以看到dlopen libdvm.so時将庫的名字改為了libvmkid_lemur.so,yunos的dalvik實作實際上在後面這個庫裡,而且通過反彙編發現導出的符号名也變化了,但内部的實作邏輯沒有變化:

dvmResolveClass -> vResolveClass
_Z18dvmFindLoadedClassPKc -> _Z18kvmFindLoadedClassPKc           

在dlsym調用時考慮以上兩種可能的符号名即可,經本地和以上問題使用者的再次驗證,已成功解決。

3. x86平台的相容性問題:

解決了yunos的相容問題後,在第二次衆測任務中,有1884位使用者參與,有3位回報異常,發現問題使用者都是x86平台的,由于最開始未對x86平台作相容,arm平台的動态庫在x86手機上運作的異常有兩種:

a) 部分手機一直卡在黑屏界面,經日志定位,這些手機都安裝了houndini的第三方庫,會自動将arm的so轉換為x86平台相容的,so加載及符号導出都沒問題,在成功擷取dvmResolveClass符号位址後,就一直卡在dvmResolveClass的調用邏輯裡,應該是houndini庫的轉換問題

b) 部分手機運作正常,但導出符号都為NULL

在提供x86平台的so後,以上兩個問題也成功解決了。

結語

本文探讨的主要是為解決更新檔 Java 方案在 dalvik 下”unexpected DEX”異常提供一個新的思路,在整個 Android 更新檔大的技術架構下,隻是其中一個環節,有問題,歡迎大家多多交流!

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

了解最新移動開發相關資訊和技術,請關注mobilehub公衆微信号(ID: mobilehub)。

QFix 探索之路 —— 手Q熱更新檔輕量級方案