[原文鏈接](https://m.study.163.com/article/1048047024?utm_campaign=share&utm_medium=androidShare&utm_u=1017421009&utm_source=qq)
前言
今年真是熱補丁框架的洪荒之力爆發的一年,短短幾個月內,已經出現了好幾個熱修復的框架了,基本上都是大同小異,這里我就不過多的去評論這些框架。只有自己真正的去經歷過,你才會發現其中的
事實上,現在出現的大多數熱修復的框架,穩定性和兼容性都還達不到要求,包括阿里的Andfix,據同事說,我們自己的app原本沒有多少crash,接入了andfix倒引起了一部分的crash,這不是一個熱修復框架所應該具有的“變態功能”。雖然阿里百川現在在大力推廣這套框架,我依舊不看好,只是其思路還是有學習價值的。
**Dex的熱修復總結**
Dex的熱修復目前來看基本上有四種方案:
* 阿里系的從native層入手,見AndFix
* QQ空間的方案,插樁,見安卓App熱補丁動態修復技術介紹
* 微信的方案,見微信Android熱補丁實踐演進之路,dexDiff和dexPatch,方法很牛逼,需要全量插入,**但是這個全量插入的dex中需要刪除一些過早加載的類**,不然同樣會報class is pre verified異常,還有一個缺點就是合成占內存和內置存儲空間。微信讀書的方式和微信類似,見Android Patch 方案與持續交付,不過微信讀書是miniloader方式,啟動時容易ANR,在我錘子手機上變現出來特別明顯,長時間的卡圖標現象。
* 美團的方案,也就是instant run的方案,見Android熱更新方案Robust
此外,微信的方案是多classloader,這種方式可以解決用multidex方式在部分機型上不生效patch的問題,同時還帶來一個好處,這種多classloader的方式使用的是instant run的代碼,如果存在native library的修復,也會帶來極大的方便。
**Native Library熱修復總結**
而native libraray的修復,目前來說,基本上有兩種方案。
* 類似multidex的dex方式,插入目錄到數組最前面,具體文章見Android熱更新之so庫的熱更新,需要處理系統的兼容性問題,系統分隔線是Android 6.0
* 第二種方式需要依賴多classloader,在構造BaseDexClassLoader的時候,獲取原classloader的native library,通過環境變量分隔符(冒號),將patch的native library與原目錄進行連接,patch目錄在前,這樣同樣可以達到修復的目的,缺點是需要依賴dex的熱修復,優點是應用native library時不需要處理兼容性問題,當然從patch中釋放出來的時候也需要處理兼容性問題。
第二種方式的實現可以看看BaseDexClassLoader的構造函數
~~~
BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent)
~~~
只需要在修復dex的同時,如果有native library,則獲取原來的路徑與patch的路徑進行連接,偽代碼如下:

而這種方式需要強依賴dex的修復,如果沒有dex,就無能為例了,實際情況基本上是兩種方式交叉使用,在沒有dex的情況下,使用另外一種方式。
而native library還有一個坑,就是從patch中釋放so的過程,這個過程需要處理兼容性,在Android 21以下,通過下面這個函數去釋放

而在andrdod 21及以上,則通過下面的這幾個函數去釋放

**資源的熱修復**
而對于資源的熱修復,其實主要還是和插件化的思路是一樣的,具體實現可以參考兩個
* Atlas或者攜程的插件化框架
* Instant run的資源處理方式,甚至可以做到運行期立即生效。
本篇文章就來說說資源的熱修復的實現思路,在這之前,需要貼兩個鏈接,以下文章的內容基于這兩個鏈接去實現,所以務必先看看,不然會一臉懵逼。一個是instant run的源碼,自備梯子,另一個是馮老師寫的一個類,這個類在Atlas中出現過,后來被馮老師重寫了,同樣自備梯子。
* instant-run源碼
* Hack.java實現
資源的熱修復實現,主要由一下幾個步驟組成:
* 提前感知系統兼容性,不兼容則不進行后續操作
* 服務器端生成patch的資源,客戶端應用patch的資源
* 替換系統AssetManger,加入patch的資源
對于第一步,我們需要先看看instant run對于資源部分的實現,其偽代碼如下

代碼很簡單,通過調用addAssetPath將patch的資源加到新建的AssetManager對象中,然后將內存中所有Resources對象中的AssetManager對象替換為新建的AssetManager對象。當然還需要處理兼容性問題,對于兼容性問題,則需要用到馮老師的Hack類(這里我為了與原來馮老師沒有重寫前的Hack類做區分,將其重命名了HackPlus,意思你懂的),具體Hack過程請參考Atlas或者攜程的插件化框架的實現,然后基于instant run進行實現,當然這種方式有一部分資源是修復不了了,比如notification。
* 主要的分界線是Android 19 和 Android N
* 首先需要拿到App運行后內存中的Resources對象
* Android N,通過ResourcesManager中的mResourceReferences去獲取Resources對象,是個ArrayList對象
* Android 19到Android N(不含N),通過ResourcesManager中的mActiveResources去獲取Resources對象,是個ArrayMap對象
* Android 19以下,通過ActivityThread的mActiveResources去獲取Resources對象,是個HashMap對象。
* 接著就是替換Resources中的AssetManager對象
* Android N,替換的是Resources對象中的mResourcesImpl成員變量中的mAssets成員變量。
* Android N以前,替換的是Resources對象中的mAssets成員變量。
* 對于Android 19以下,ActivityThread是通過ActivityThread中的靜態函數currentActivityThread獲取的的,這里有個坑,如果在主線程獲取還好,但是萬一在子線程獲取,在低版本的Android上可能就是Null,因為在低版本,這個變量是通過ThreadLocal進行存儲的,對于這種情況,只要檢測當前線程是不是主線程,如果是主線程,則直接獲取,如果不是主線程,則阻塞當前線程,然后切換到主線程獲取,獲取完成后通知阻塞線程。
這里我已經基本實現了反射檢測系統支持性相關的代碼,主要就是對以上分析的內容做反射檢測,一旦發生異常,則不再進行資源的修復,代碼如下(HackPlus的源碼見上面的Hack.Java的源碼):







使用的時候,只要在加載patch資源前,調用如下方法進行檢測

patch資源的生成比較麻煩,我們放在最后面說明,現在假設我們有一個包含整個apk的資源的文件,需要運行時替換,現在來實現上面的加載patch資源的邏輯,具體邏輯上面反射的時候已經說明了,這時候只需要調用上面反射獲取的包裝類,進行替換即可,直接看代碼中的注釋:



這樣一來,就在Appliction啟動的時候完成了資源的熱修復,當然我們也可以像instant run那樣,把activity也處理,不過我們簡單起見,讓其重啟生效,所以activity就不處理了。
于是,我們Appliction的onCreate()中的代碼就變成了下面這個樣子

這里有一個坑。
> patch應用成功后,如果要刪除patch,patch文件的刪除一定要謹慎,最好先通過配置文件標記patch不可用,下次啟動時檢測該標記,然后再刪除,運行期刪除正在使用的patch文件會導致所有進程的重啟,Application中的所有邏輯會被初始化一次。
還差最后一步,patch的資源從哪里來,這里主要講兩種方式。
* 直接下發整個apk文件,全量的資源,想怎么用就怎么用,當然缺點很明顯,文件太大了,下載容易出錯,不過也最簡單。
* 下發patch部分的資源,在客戶端和沒改變的資源合成新的apk,這種方式的優點是文件小,缺點是合成時占內存,需要開啟多進程去合成,比較復雜,沒有辦法校驗合成文件的md5值。
**無論哪一種方式,都需要public.xml去固定資源id。**
這里討論的是第二種方式,所以給出精簡版的實現思路:
首先需要生成public.xml,public.xml的生成通過aapt編譯時添加-P參數生成。相關代碼通過gradle插件去hook Task無縫加入該參數,有一點需要注意,通過appt生成的public.xml并不是可以直接用的,該文件中存在id類型的資源,生成patch時應用進去編譯的時候會報resource is not defined,解決方法是將id類型的資源單獨記錄到ids.xml文件中,相當于一個聲明過程,編譯的時候和public.xml一樣,將ids.xml也參與編譯即可。


在編譯資源之前,將public.xml和ids.xml文件拷貝到資源目錄values下,并檢測values.xml文件中是否有已經定義的id類型的資源,如果有,則從ids.xml文件中將其刪除,否則會報resource is already defined的異常,也會編譯不過去。



這樣一來,按照正常流程去編譯,生成的apk安裝包就可以獲得了,然后將這個new.apk和有問題的old.apk進行差量算法,這里只考慮資源相關文件,即assets目錄,res目錄,arsc文件,AndroidManifest.xml文件,相關算法如下:
* 對比new.apk和old.apk中的所有資源相關的文件。
* 對于新增資源文件,則直接壓入patch.apk中。
* 對于刪除的資源文件,則不處理到patch.apk中。
* 對于改變的資源文件,如果是assets或者res目錄中的資源,則直接壓縮到patch.apk中,如果是arsc文件,則使用bsdiff算法計算其差量文件,壓入patch.apk,文件名不變。
* 對于改變和新增的文件,通過一個meta文件去記錄其原始文件的adler32和合成后預期文件的adler32值,以及文件名,這是個文本文件,直接壓縮到patch.apk中去。
* 對patch.apk進行簽名。
這樣做的好處是能將資源patch文件盡可能的減小到最低,實際情況嚴重下來,res目錄下的資源文件大小都非常小,沒有必要去進行diff,所以直接使用原文件,而arsc文件則相對比較大,在考慮文件大小和內存的兩個因素下,犧牲內存換大小還是ok的,所以在下發前,我們對其進行diff,生成diff文件,在客戶端進行合成最終的arsc文件。
客戶端下載到patch.apk后需要進行還原,還原的步驟如下:
* 考慮到客戶端jni的兼容性問題,bspatch算法全部使用java實現
* 首先校驗patch.apk的簽名
* 讀取壓縮包中meta文件,判斷哪些文件是新增文件,哪些文件是改變的文件。
* 遍歷patch.apk中的文件,如果是新增文件,則壓縮到new.apk文件中去
* 如果是改變的文件,如果是assets和res文件夾下的資源,則直接壓縮到new.apk文件中,如果是arsc文件,則應用bspatch算法合成最終的arsc文件,壓縮到new.apk中
* 如果文件沒有改變,則直接復制old.apk中的原始文件到new.apk中
* 以上任何一個步驟都會去校驗合成時舊文件的adler32和合成后的adler32值和meta文件中記錄的是否符合
* 由于無法驗證合成后的文件的md5值(沒有記錄哪些文件被刪除了,加上壓縮算法等原因),需要使用一種方式在加載前進行驗證,這里使用crc32值。
* 合成成功后計算new.apk文件的crc32值,計算方式進行改進,不計算所有文件內容的crc32,為了快速計算,只計算文件的某一個特定段的crc32值,比如文件從200字節開始到2000字節部分的crc32值,并保存在sharePrefrences中,加載patch前進行校驗crc32,校驗不通過,則直接刪除patch文件,當然這種計算方式有一定概率會把錯誤的文件當成正確的,畢竟計算的不是完整的文件,當然正確的文件是一定不會當成錯誤的,這種低概率事件可以接受。
這種方式的兼容性如何?簡單自測了下,4.0-7.0的模擬器運行全部通過,當然不排除國產奇葩ROM的兼容性,所以這里我不宣稱100%兼容。
無圖言屌,沒圖你說個jb,先上一張沒有進行熱修復的圖:

熱修復之后的效果圖

最后送上一句話:
熱修復遠遠沒有你想象的那么簡單,踩坑之路漫漫,入坑需謹慎。
- 前言
- Android 熱補丁技術——資源的熱修復
- 插件化系列詳解
- Dex分包——MultiDex
- Google官網——配置方法數超過 64K 的應用
- IMOOC熱修復與插件化筆記
- 第1章 class文件與dex文件解析
- Class文件解析
- dex文件解析
- class與dex對比
- 第2章 虛擬機深入講解
- 第3章 ClassLoader原理講解
- 類的加載過程
- ClassLoade源碼分析
- Android中的動態加載
- 第4章 熱修復簡單講解
- 第5章 熱修復AndFix詳解
- 第6章 熱修復Tinker詳解及兩種方式接入
- 第7章 引入熱修復后代碼及版本管理
- 第8章 插件化原理深入講解
- 第9章 使用Small完成插件化
- 第10章 使用Atlas完成插件化
- 第11章 課程整體總結
- DN學院熱修復插件化筆錄
- 插件化
- 熱修復
- Android APP開發應掌握的底層知識
- 概述
- Binder
- AIDL
- AMS
- Activity的啟動和通信原理
- App啟動流程第2篇
- App內部的頁面跳轉
- Context家族史
- Service
- BroadcastReceiver
- ContentProvider
- PMS及App安裝過程