這篇文章是基於內部分享的逐字稿內容整理的,現在比較喜歡寫逐字稿,方便整理成文章。
文章目錄
好,那我們開始。
兩週前 HG 分享了 QQ 空間的熱修復框架,今天我來簡單講一下微信開源的熱修復框架,Tinker。
-
目錄
-
特點介紹
-
使用
-
patch 加載,分別講下 dex resources 和 so
-
patch 的產生和合並
目錄
講的內容主要分爲以下幾節:
- 第一節: 特點介紹,簡單介紹一下 Tinker 和其他熱門框架對比的優點。
- 第二節:通過官方提供的 sample 瞭解 Tinker 的使用和基礎 API
- 知道怎麼用以後,我們再一起探究一下背後的原理
- 第三節:瞭解下運行時 Tinker 是如何加載補丁的,分爲 dex,資源和 so 庫
- 第四節:瞭解一下 patch 的格式和如何做 diff,以及運行時如何合成
- 時間夠的話簡單講下 gradle plugin
- 第五節:總結
這次分析基於的是目前最新的
1.9.14.3
版本
ok 我們首先來了解一下 Tinker。
Tinker 介紹
Tinker 是微信開源的熱修復框架,Github 主頁在 https://github.com/Tencent/tinker/wiki。
我們看下 Tinker 和其他熱門框架的對比圖:
可以看到,Tinker 的特點是:
- 支持類替換、So 替換,資源替換是採用類似 instant-run 的方案
- 補丁包較小,自研 diff 方案,下發的是差量包,包括的是變更的內容
- 支持 gradle,提供了 gradle-plugin,允許我們配置很多內容
- 採用全量 Dex 更新,不需要額外處理
CLASS_ISPREVERIFIED
問題
CLASS_ISPREVERIFIED
問題大家可能也知道,如果 A 類引用的類都在一個 dex 裏,就會被打上preverified
,後面如果有引用其他 dex 的類,就會報錯。(這句可以不說:這種情況只發生在 Dalvik 虛擬機上)QZone 採用的方案通過字節碼插樁讓可能被修復的類都不打上這個標誌,會導致有性能影響。Tinker 在合併 dex 時,會創建一個新的幾乎完整的 dex,從而規避了這個問題。
具體細節等下講原理的時候說。
Tinker 還有一個優點就是一直在維護中,迭代更新還比較快。
缺點主要就是不支持即時生效。因爲是在 Java 層做修復,而不是 native 層。
ok,簡單瞭解了 Tinker 的特點後,我們來看下 Tinker 的使用。
使用
- gradle 配置
- 核心類介紹
TinkerApplicaition
ApplicationLike
TinkerLoader
PatchListener
TinkerResultService
Reporter
這裏使用的是官方提供的 sample,我們來看下接入 Tinker 需要做哪些。
(打開 tinker-sample-android)
首先打開根目錄的 `build.gradle,可以看到,這裏依賴了
tinker-patch-gradle-plugin``:
這個插件主要做的是提供了五個核心 Task,在編譯期間對資源和代碼做一些額外處理
接着打開 app
目錄下的 build.gradle
文件,可以看到對 tinker 的依賴有三個:
tinker-android-lib
,這個主要是提供對外暴露的 API,等下使用到的 Tinker API 基本都在這個工程下tinker-android-loader
,這個工程主要是完成 patch 的加載,稍後講解 patch 加載原理時主要講的就是這個工程tinker-android-anno
,這個工程很簡單,就是一個註解處理器,作用就是幫助我們生成一個Applicaition
,可以看下它的代碼(讀取註解的信息,根據模板信息生成一個類)
添加了依賴後,還需要添加一些配置信息,我們繼續看 build.gradle
。
首先看到 ext 拓展屬性裏定義了幾個屬性,
def bakPath = file("${buildDir}/bakApk/")
/**
* you can use assembleRelease to build you base apk
* use tinkerPatchRelease -POLD_APK= -PAPPLY_MAPPING= -PAPPLY_RESOURCE= to build patch
* add apk from the build/bakApk
*/
ext {
//for some reason, you may want to ignore tinkerBuild, such as instant run debug build?
tinkerEnabled = true
//for normal build
//old apk file to build patch apk
tinkerOldApkPath = "${bakPath}/app-debug-0424-15-02-56.apk"
//proguard mapping file to build patch apk
tinkerApplyMappingPath = "${bakPath}/app-debug-1018-17-32-47-mapping.txt"
//resource R.txt to build patch apk, must input if there is resource changed
tinkerApplyResourcePath = "${bakPath}/app-debug-0424-15-02-56-R.txt"
//only use for build all flavor, if not, just ignore this field
tinkerBuildFlavorDirectory = "${bakPath}/app-1018-17-32-47"
}
tinkerEnabled
和名字一樣表示使用啓用 tinkertinkerOldApkPath
表示基準包的位置,這裏的bakPath
就是app/build/bakApk
目錄tinkerApplyMappingPath
表示基準包使用混淆時的 mapping 文件所在路徑,在做差量包時需要使用這個 mappingtinkerApplyResourcePath
表示資源 R.txt 的路徑,這個文件在 build 階段處理資源processDebugResources
時,會生成資源索引等信息輸出到這個文件
翻到下面我們可以看到,這裏有些一個 task 在編譯生成 apk 後會拷貝 apk mapping 和 R.txt 文件到這個 bak 目錄下
ok 接下來看一下最關鍵的 tinker-gradle 配置。
https://github.com/Tencent/tinker/wiki/Tinker-%E6%8E%A5%E5%85%A5%E6%8C%87%E5%8D%97
tinkerPatch
是 tinker 的拓展屬性,允許我們對 build 過程做一些自定義。
(簡單介紹兩個比較重要的配置)
buildConfig
裏的是編譯相關的配置keepDexApply
是指開啓補丁包根據基準包的類分部進行編譯,避免補丁修改很多,導致類所在的 dex 和基準包不一樣isProtectedApp
是否使用加固模式,這種情況下只將變更的類合成補丁dex
是對 dex 裏的配置dexMode
,輸入的 dex 格式,jar 或者 rawpattern
需要處理的 dex 路徑loader
是配置一些不會打入 patch 的類,默認放加載插件相關的類
其他的配置還有很多,這裏就不 一一介紹了。
ok 瞭解了 gradle 文件的配置內容後,我們來看下項目代碼。
首先看下 AndroidManifest.xml
文件:
可以看到這個 sample 比較簡單,先看下這個 Applicaition
,這個類在 build 目錄,就是我們前面提到的,通過 註解處理器生成的類。
這個 SampleApplication
繼承了 TinkerApplicaitiopn
,我們看下代碼。
TinkerApplicaition
TinkerApplicaitiopn
需要講解的點:
- 構造函數參數的意義
tinkerFlags
表示要加載的類型,包括 dex , library 還是全部支持delegateClassName
表示 Applicaition 代理類的 className,也就是 ApplicaitionLike- 第三個參數
loaderClassName
表示 Tinker 加載類的 className,默認是TinkerLoader
,我們也可以繼承做些修改 - 最後一個參數
tinkerLoadVerifyFlag
表示是否需要在加載時檢查文件的 md5,默認是 false,因爲在合成階段就做了校驗,所以這裏一般不需要再校驗
- 做了什麼,先看
attachBaseContext()
- 反射創建
TinkerLoader
,調用TinkerLoader#tryLoad
方法加載補丁,具體細節稍後講解 - 創建橋接類,回調橋接類各個生命週期方法
- 這裏我們可以看到,是通過反射創建一個橋接類,在橋接類裏又通過反射創建了代理類,原因我們等下講
SampleApplicaitionLike
ok,我們接下來看一下 sample 裏的 Application 代理類 SampleApplicaitionLike
,它使用了 @DefaultLifeCycle
註解,參數就是要生成的 Applicaition 全路徑,支持類型是全部,加載驗證是 false。
SampleApplicaitionLike
繼承了 DefaultApplicaitionLike
,提供了一些類似 Application 的 API。裏面也沒有做什麼額外處理,只是爲了讓我們把在 Applicaition 裏的代碼轉移到這裏。
爲什麼要通過這種方式呢?
主要有兩個原因:
- 讓 Tinker 可以對 Applicaition 初始化時使用到的類進行修復
- 應用 7.0 混合編譯對熱修復的影響
官方文檔裏的介紹是這樣說的:
程序啓動時會加載默認的 Application 類,這導致我們補丁包是無法對它做修改了。
如何規避?
在這裏我們並沒有使用類似InstantRun hook Application的方式,而是通過代碼框架的方式來避免,這也是爲了儘量少的去反射,提升框架的兼容性。
這裏我們要實現的是完全將原來的Application類隔離起來,即其他任何類都不能再引用我們自己的Application。我們需要做的其實是以下幾個工作:
- 將我們自己Application類以及它的繼承類的所有代碼拷貝到自己的ApplicationLike繼承類中,例如SampleApplicationLike。你也可以直接將自己的Application改爲繼承ApplicationLike;
- Application的attachBaseContext方法實現要單獨移動到onBaseContextAttached中;
- 對ApplicationLike中,引用application的地方改成getApplication();
- 對其他引用Application或者它的靜態對象與方法的地方,改成引用ApplicationLike的靜態對象與方法;
也就是說,通過反射,將Tinker組建和App隔離開,並且先後順序是先Tinker後App,這樣可以防止App中的代碼提前加載,確保App中所有的代碼都可以具有被熱修復的能力包括ApplicationLike。
ok 回到 SampleApplicationLike
中,可以看到在 onBaseContextAttached()
方法中,調用了 TinkerManager#installTinker
進行初始化,然後調用 Tinker#with
初始化 Tinker 實例。
TinkerManager#installTinker
裏有創建了一些自定義的監聽器,包括 patch 加載監聽、patch 驗證監聽 、收到 patch 的監聽等
TinkerInstaller
TinkerInstaller#install
裏創建了 Tinker 類,調用了 install 方法。
TinkerInstaller#onReceiveUpgradePatch
方法,在接收到新的 patch 後我們調用這個方法,傳入路徑,然後會進行 patch 的合成,我們稍後介紹。
還有其他監聽類我們就不一一介紹了。
PatchListener
TinkerResultService
Reporter
OK,那我們接下來把項目運行起來看看效果。
有提供腳本 push 到設備
總結
Tinker流程圖
ok,瞭解了 Tinker 的基本使用後,我們來看下背後的原理。
tinker.png
這張圖來自 Tinker Github。
Tinker 將 old.apk 和 new.apk 做了 diff,生成一個 patch.dex,然後下發到手機,將 patch.dex 和本機 apk 中的 classes.dex 做了合併,生成新的 classes.dex,然後加載。
首先看下 Tinker 加載補丁的代碼。因相交於生成,這部分更簡單些。
運行時 Tinker 是如何加載補丁
前面我們在介紹生成的 Application
時就提到,TinkerApplication#attachBaseContext
中輾轉會調用到 loadTinker()
方法,在該方法內部,反射調用了 TinkerLoader#tryLoad
方法加載 patch。
TinkerLoader
相關的代碼在 tinker-android-loader
:
看下加載相關的類圖。TinkerLoader
是加載對外暴露的 API,它內部調用了 TinkerDexLoader
, TinkerResourceLoader
和 TinkerSoLoader
分別用於加載 dex 資源和 so。
我們看下代碼, TinkerLoader#tryLoad
,它調用了 TinkerLoader#trylLoadPatchFilesInternal
,這個方法內容很多,主要做了兩件事:
- 會判斷補丁是否存在,檢查補丁信息中的數據是否有效,校驗補丁簽名以及tinkerId與基準包是否一致。在校驗簽名時,爲了加速校驗速度,Tinker只校驗 *_meta.txt文件,然後再根據meta文件中的md5校驗其他文件
- 根據開發者配置的Tinker可補丁類型判斷是否可以加載dex,res,so。然後分別分發給TinkerDexLoader、TinkerSoLoader、TinkerResourceLoader分別進行校驗是否符合加載條件進而進行加載
很多校驗我們就不細看了。主要看下對 TinkerDexLoader
, TinkerResourceLoader
和 TinkerSoLoader
的調用。
TinkerLoader
裏搜TinkerDexLoader
首先看下 TinkerDexLoader 是如何校驗、加載的 dex。
加載 dex
TinkerDexLoader 的兩個方法:
TinkerDexLoader#checkComplete
檢查 dex 補丁文件和優化過的 odex 文件是否可以加載TinkerDexLoader#loadTinkerJar
把補丁 dex 插入到 ClassLoader 裏
我們重點看下 TinkerDexLoader#loadTinkerJar
。
選中
classloader
變量
前面都是做一些校驗和 OTA 的處理,直接看方法的最後,調用了 SystemClassLoaderAdder#installDexes
這個核心方法。
點進去看一下,可以看到,它區分不同版本做了不同的處理。
我們重點看下 19 和 24 的處理。
- V19
- 找到 classLoader.pathList
- 然後調用
makeDexElements
創建新的 Element[] 數組 - 然後調用
ShareReflectUtil#expandFieldArray
插入,重點看下這個方法 - 創建一個新的數組,把 patch 相關的 dex 放在前面,然後將合併後的數組設置給 pathList
- V23
- 和 19 的區別在於
makeDexElements
->makePathElements
,方法的名稱、參數做了調整 - V24 (7.0)的特殊點在於
- 爲每個 patch 創建了一個新的
AndroidNClassLoader
- 這是由於安卓 7.0 支持混合編譯 ,混合編譯對熱修復的影響
簡單瞭解下混合編譯。
混合編譯與熱修復
- 爲什麼有混合編譯
- 混合編譯對熱修復的影響
- 如何解決
我們知道:
- Dalvik 虛擬機,運行時通過 JIT 把字節碼編譯成機器碼再執行,運行時速度比較慢
- ART 虛擬機上,改爲 AOT 提前編譯 ,即在安裝應用或者 OTA 系統升級的時候把字節碼翻譯 成 機器碼,這樣運行時就可以直接執行了
- 但 ART 的缺點是,可能會導致安裝時間過長,而且全量編譯佔用的 ROM 空間更大
所以在 Android N 上提出了混合編譯,AOT 編譯, JIT 編譯和解釋執行配合使用。
在應用運行時分析運行過的代碼以及“熱代碼”,並將配置存儲下來。在設備空閒與充電時,ART僅僅編譯這份配置中的“熱代碼”
簡單來說,就是在應用首次安裝、運行時不做 AOT 編譯,然後把運行中 JIT 解釋執行的代碼記錄下來,設備空閒時通過 dex2oat
編譯生成名爲 app_image
的 base.art
文件,這個文件主要爲了 加快應用對“熱代碼”的加載和緩存。
在 apk 啓動時,會加載應用的 oat 文件和可能存在的 app_image 文件,如果存在 app_image 文件,則把這個文件裏的 class 插入到 ClassTable,在類加載時,會先從 ClassTable 中查找,找不到纔會去走 defineClass
app image的作用是記錄已經編譯好的“熱代碼”,並且在啓動時一次性把它們加載到緩存。預先加載代替用時查找以提升應用的性能,到這裏我們終於明白爲什麼base.art會影響熱補丁的機制。
無論是使用插入pathlist還是parent classloader的方式,若補丁修改的class已經存在於 app image,它們都是無法通過熱補丁更新的。它們在啓動app時已經加入到PathClassloader的ClassTable中,系統在查找類時會直接使用base.apk中的class。
從剛纔的代碼我們也看到了,Tinker 的解決方案是,新建一個 ClassLoader,也就是不使用之前的 cache。
可以看到,加載 dex 其實和 QZone 的方案差不多,都是通過反射將 dex 文件放置到加載的 dexElements 數組的前面。
微信Tinker原理圖
區別在於:
- QZone 是將 patch.dex 插到數組的前面,也就是說沒修改的類還是在之前的 dex 裏,這就可能導致那個
CLASS_ISPREVERIFIED
問題,QZone 通過插樁解決這個問題,這裏就不多說了 - Tinker 則是將合併後的全量 dex 插在數組前,這樣就避免了這個問題的出現
加載資源
Tinker的資源更新採用的
InstantRun
的資源補丁方式,全量替換資源
首先回顧一下,應用加載資源是通過 Context.getResources()
返回的 Resources 對象, Resources 內部包裝了 ResourcesImpl
, 間接持有了 AssetManager
對象,最終由 AssetManager 從 apk 文件中加載資源。
要加載資源,需要做 2 步:
- 新建一個 AssetManager ,通過
addAssetPath()
方法把補丁資源目錄傳遞進去 - 替換所有 Resources 對象中的 AssetManager
看下代碼,資源加載部分主要在 TinkerResourceLoadr
中,兩個方法:
TinkerResourceLoader#checkComplete
檢查資源補丁是否存在,存在的話,調用TinkerResourcePatcher#isResourceCanPatch
區分版本拿到Resources
對象的集合,同時創建新 AssetsManager- 看一下代碼
- 拿到
addAssetPathMethod
方法留着後面調用 - 4.4 以上通過
ResourcesManager
獲取mActiveResources
變量,它是 ArrayMap 類型;在 7.0 上這個變量名稱爲mResourceReferences
- 4.4 以下通過
ActivityThread
獲取mActiveResources
變量,是一個 HashMap - 保存這些集合後
TinkerResourceLoader#loadTinkerResource
調用TinkerResourcePatcher#monkeyPatchExistingResources
- (這個方法的名字跟 InstantRun 的資源補丁方法名是一樣的)
- 反射調用新建的
AssetManager#addAssetPath
將路徑穿進去 - 循環遍歷持有Resources對象的references集合,依次替換其中的AssetManager爲新建的AssetManager
- 最後調用Resources.updateConfiguration將Resources對象的配置信息更新到最新狀態,完成整個資源替換的過程
加載補丁要替換的Resources對象在KITKAT之下是以HashMap的類型作爲ActivityThread類的屬性.其餘的系統版本都是以ArrayMap被ResourcesManager持有的.所以要按照系統區分開.
市面上大多數的熱補丁框架都採用 instant-run 的這套資源更新方案
加載 so
Tinker 加載 SO 補丁提供了兩個入口,
TinkerLoadLibraryloadArmLibrary
TinkerApplicationHelper#loadLibraryFromTinker
(看下它的代碼)
比較簡單,最終都是調用 System#load
dex diff
- dex 格式
- diff 簡單思路
- 代碼
Tinker 的亮點之一就是它的 diff 算法,補丁裏只包含改變的信息,非常小。這一節我們來了解下如何實現的 dex diff。
開始之前,先簡單介紹下 dex 的格式。
dex 格式
先 javac 生成 class 文件,再通過 dx 工具生成 dex 文件。
dx --dex --output=Hello.dex Hello.class
如圖所示,dex 文件主要包括三個區域:
文件頭記錄了包含了一些校驗相關的字段,和整個dex文件大致區塊的分佈
結合 010Editor 打開的 HelloWorld.dex 文件介紹下內容。
header 定長 112。
magic
是用於表示 dex 文件和版本。checksum
是文件檢驗和- 其餘的根據名字就可以看出來意義
成對出現的size和off,大多代表各區塊的包含的特定數據結構的數量和偏移量。例如:string_ids_off爲112,指的是偏移量112開始爲string_ids區域;string_ids_size爲14,代表string_id_item的數量爲14個
緊接着 header 後面的是索引區,描述了 dex 文件中 各種格式的數據和 id。
- string_id_list,描述了 dex 文件中所有的字符串,格式很簡單隻有一個偏移量,偏移量指向了 string_data 區段的一個字符串
- type_id_list,描述 dex 中所有的文件類型,內容也很簡單,只有一個 string_id 裏的序號
- proto_id_list,描述了方法原型,內容包括 method 原型名稱、返回值和參數,參數 0 表示沒有
- field_id_list,描述了 dex 文件引用的所有 field,內容包括 field 的所在的 class,field 的類型和 field 的名稱
- method_id_list,描述了 dex 文件裏所有的 method,內容包括方法所屬的 class,類型和名稱
最後是數據區,010 Editor 中沒有展示 data 的數據。
- class_def,描述了 dex 文件中的 class 信息
- map_list(對應上圖中的 link_data ),大部分 item 跟 header 中的相應描述相同,都是介紹了各個區的偏移和大小,但是 map_list 中描述的更加全面,包括了 HEADER_ITEM 、TYPE_LIST、STRING_DATA_ITEM、DEBUG_INFO_ITEM 等信息。
- 類型
- 個數
- 偏移量
- **通過
map_list
,可以將一個完整的dex文件劃分成固定的區域(本例爲13),且知道每個區域的開始,以及該區域對應的數據格式的個數
瞭解了 dex 格式後,看下 tinker 中講 dex 文件讀取到內存中的類 TableOfContents
,可以看到,使用 Section 描述不同類型的區域。
tinker patch 格式瞭解
tinker dex format
tinker patch 裏主要包括兩部分內容 :
- header,magic 和 version ,記錄各個 Section 的偏移量
- 其餘部分是不同類型的變更情況,item 包括操作類型、索引和變更內容
使用
java -jar tinker-dex-dump.jar --dex classes.dex
可以看到,patch 主要記錄的是對不同數據類型的數據進行的新增、刪除或者修改操作,和修改的內容。
對應 tinker 裏的 PatchOperation
類:
public final class PatchOperation<T> {
public static final int OP_DEL = 0;
public static final int OP_ADD = 1;
public static final int OP_REPLACE = 2;
public int op;
public int index;
public T newItem;
}
diff 和合並簡單思路
瞭解 tinker-patch 的內容後,就基本可以瞭解到 tinker dex-diff 的思路了。
逐個對比新舊 dex 每個 Section 的變更情況,然後再 patch 裏把每個區域變更的類型和索引、內容寫到 patch 裏。
運行時拿到 patch,根據變更 Section 裏的數據,去修改對應的索引的數據,生成最終 dex。
看看代碼是不是這樣。
源碼分析
前面的例子我們知道,在執行完 tinker 的 tinkerPatchDebug
task 後 ,就生成了 patch。
順着代碼看下,
- TinkerPatchSchemaTask
- Runner#tinkerPatch
- ApkDecoder#patch
- DexDiffDecoder#onAllPatchesEnd
- DexDiffDecoder#generatePatchInfoFile
- DexDiffDecoder#diffDexPairAndFillRelatedInfo
- DexPatchGenerator#executeAndSaveTo
最後發現,真正生成 patch 是在 DexPatchGenerator
這個類中。
DexPatchGenerator
-
dex 讀取到內存,
Dex#loadFrom
,TableOfContents#readFrom
,將 dex 文件內容,按照 map-list 分到不同的 Section 中 -
DexPatchGenerator
構造函數,初始化了 15 個對不同區域的算法,目的就是前面說的,計算出每個區域的變更情況 -
DexPatchGenerator#executeAndSaveTo
,調用 15 個算法的execute()
和simulatePatchOperation()
以 stringDataSectionDiffAlg
爲例,看下做了什麼。
DexSectionDiffAlgorithm
看下它的 execute()
和 simulatePatchOperation()
方法 。
execute()
做的工作:
- 拿到舊、新 dex 的 Section Item 列表,排序
- 遍歷,對比每個 item
- 做一些排序處理,合併相同 index 的 ADD 和 DEL 爲 REPLACE
- 把不同類型的操作,保存到三個 map
經歷完成 execute
之後,我們主要的產物就是 3 個Map,
indexToDelOperationMap,indexToAddOperationMap,indexToReplaceOperationMap
分別記錄了:oldDex 中哪些index需要刪除;newDex中新增了哪些item;哪些item需要替換爲新item。
simulatePatchOperation()
做的工作:根據前面的 3 個 map,計算變更數據 index 和 offset,計算下一個 Section 需要依賴前面的 offset。
經過這兩個方法 ,得到了這個 Section 的 patchOperationList
和 patchSectionSize
。
執行完所有算法,就可以得到整個 patch 所有 Section 的變更操作和對應的偏移量,
寫 patch 文件
執行完所有算後,進入 DexPatchGenerator#writeResultToStream
生成 patch 文件。
DexPatchGenerator#writePatchOperations
中,主要完成三步(看代碼):
可以看到,和我們前面看的那個圖對應的數據一樣。
patch 的合成
前面提到,TinkerInstaller#onReceiveUpgradePatch
方法,在接收到新的 patch 後我們調用這個方法,傳入路徑,然後會進行 patch 的合成。
補丁合成在單獨的 patch 進程工作,包括 dex,so 還有資源,主要完成補丁包的合成以及升級。
patch 裏關於變更信息的數據:
1.del操作的個數,每個del的index
2.add操作的個數,每個add的index
3.replace操作的個數,每個需要replace的index
4.最後依次寫入newItemList.
DexPatchApplier
- Dex File ->
DexPatchFile
- 檢查 patch 是否能作用到當前的 oldDex(通過比較 oldDex 簽名和 patch 裏記錄的 oldDex 簽名,簽名使用
SHA-1
算法) - 把 patch 裏記錄的合併後的各個 Section 的值複製給合併後 dex 的
TableOfContents
- 創建 15 個合併算法處理器,處理不同區域的數據合併
- 最後,寫入 header mapList 和 合併後 dex 的簽名和校驗和
每個 Section 的合併算法類似,繼承自 DexSectionPatchAlgorithm
:
- 讀取保存 del add replace 操作 index 的數組
- 計算出合併後的 itemCount:
newItemCount
=oldItemCount
-deleteItemCount
+addItemCount
- 往合併後的 dex 對應的 xxData 區域寫最終內容(包括沒變的、新增的和替換的)
從 0 開始,按順序寫合併後的內容規則:
- 首看先這個位置是否有新增的
- 然後看這個位置是否需要替換爲 newItem
- 如果這個位置包含在 del/replace 數組裏,那就跳過這個位置 oldItem
- 否則說明這個位置的 oldItem 元素沒動,寫到合併後的 dex 裏