微信熱修復框架 Tinker 從使用到 patch 加載、生成、合成原理分析

這篇文章是基於內部分享的逐字稿內容整理的,現在比較喜歡寫逐字稿,方便整理成文章。

好,那我們開始。

兩週前 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 的特點是:

  1. 支持類替換、So 替換,資源替換是採用類似 instant-run 的方案
  2. 補丁包較小,自研 diff 方案,下發的是差量包,包括的是變更的內容
  3. 支持 gradle,提供了 gradle-plugin,允許我們配置很多內容
  4. 採用全量 Dex 更新,不需要額外處理 CLASS_ISPREVERIFIED 問題

CLASS_ISPREVERIFIED 問題大家可能也知道,如果 A 類引用的類都在一個 dex 裏,就會被打上 preverified,後面如果有引用其他 dex 的類,就會報錯。(這句可以不說:這種情況只發生在 Dalvik 虛擬機上)QZone 採用的方案通過字節碼插樁讓可能被修復的類都不打上這個標誌,會導致有性能影響。Tinker 在合併 dex 時,會創建一個新的幾乎完整的 dex,從而規避了這個問題。

具體細節等下講原理的時候說。

Tinker 還有一個優點就是一直在維護中,迭代更新還比較快。

在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

缺點主要就是不支持即時生效。因爲是在 Java 層做修復,而不是 native 層。

ok,簡單瞭解了 Tinker 的特點後,我們來看下 Tinker 的使用。

使用

  1. gradle 配置
  2. 核心類介紹
  • 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 和名字一樣表示使用啓用 tinker
  • tinkerOldApkPath 表示基準包的位置,這裏的 bakPath 就是 app/build/bakApk 目錄
  • tinkerApplyMappingPath 表示基準包使用混淆時的 mapping 文件所在路徑,在做差量包時需要使用這個 mapping
  • tinkerApplyResourcePath 表示資源 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 或者 raw
  • pattern 需要處理的 dex 路徑
  • loader 是配置一些不會打入 patch 的類,默認放加載插件相關的類

其他的配置還有很多,這裏就不 一一介紹了。

ok 瞭解了 gradle 文件的配置內容後,我們來看下項目代碼。

首先看下 AndroidManifest.xml 文件:

在這裏插入圖片描述

可以看到這個 sample 比較簡單,先看下這個 Applicaition,這個類在 build 目錄,就是我們前面提到的,通過 註解處理器生成的類。

這個 SampleApplication 繼承了 TinkerApplicaitiopn,我們看下代碼。

TinkerApplicaition

TinkerApplicaitiopn 需要講解的點:

  1. 構造函數參數的意義
  • tinkerFlags 表示要加載的類型,包括 dex , library 還是全部支持
  • delegateClassName 表示 Applicaition 代理類的 className,也就是 ApplicaitionLike
  • 第三個參數 loaderClassName 表示 Tinker 加載類的 className,默認是 TinkerLoader,我們也可以繼承做些修改
  • 最後一個參數 tinkerLoadVerifyFlag 表示是否需要在加載時檢查文件的 md5,默認是 false,因爲在合成階段就做了校驗,所以這裏一般不需要再校驗
  1. 做了什麼,先看 attachBaseContext()
  • 反射創建 TinkerLoader,調用 TinkerLoader#tryLoad 方法加載補丁,具體細節稍後講解
  • 創建橋接類,回調橋接類各個生命週期方法
  • 這裏我們可以看到,是通過反射創建一個橋接類,在橋接類裏又通過反射創建了代理類,原因我們等下講

SampleApplicaitionLike

ok,我們接下來看一下 sample 裏的 Application 代理類 SampleApplicaitionLike,它使用了 @DefaultLifeCycle 註解,參數就是要生成的 Applicaition 全路徑,支持類型是全部,加載驗證是 false。

SampleApplicaitionLike 繼承了 DefaultApplicaitionLike,提供了一些類似 Application 的 API。裏面也沒有做什麼額外處理,只是爲了讓我們把在 Applicaition 裏的代碼轉移到這裏。

爲什麼要通過這種方式呢?

主要有兩個原因:

  1. 讓 Tinker 可以對 Applicaition 初始化時使用到的類進行修復
  2. 應用 7.0 混合編譯對熱修復的影響

官方文檔裏的介紹是這樣說的:

程序啓動時會加載默認的 Application 類,這導致我們補丁包是無法對它做修改了。
如何規避?
在這裏我們並沒有使用類似InstantRun hook Application的方式,而是通過代碼框架的方式來避免,這也是爲了儘量少的去反射,提升框架的兼容性。
這裏我們要實現的是完全將原來的Application類隔離起來,即其他任何類都不能再引用我們自己的Application。我們需要做的其實是以下幾個工作:

  1. 將我們自己Application類以及它的繼承類的所有代碼拷貝到自己的ApplicationLike繼承類中,例如SampleApplicationLike。你也可以直接將自己的Application改爲繼承ApplicationLike;
  2. Application的attachBaseContext方法實現要單獨移動到onBaseContextAttached中;
  3. 對ApplicationLike中,引用application的地方改成getApplication();
  4. 對其他引用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, TinkerResourceLoaderTinkerSoLoader 分別用於加載 dex 資源和 so。

在這裏插入圖片描述

我們看下代碼, TinkerLoader#tryLoad ,它調用了 TinkerLoader#trylLoadPatchFilesInternal ,這個方法內容很多,主要做了兩件事:

  1. 會判斷補丁是否存在,檢查補丁信息中的數據是否有效,校驗補丁簽名以及tinkerId與基準包是否一致。在校驗簽名時,爲了加速校驗速度,Tinker只校驗 *_meta.txt文件,然後再根據meta文件中的md5校驗其他文件
  2. 根據開發者配置的Tinker可補丁類型判斷是否可以加載dex,res,so。然後分別分發給TinkerDexLoader、TinkerSoLoader、TinkerResourceLoader分別進行校驗是否符合加載條件進而進行加載

很多校驗我們就不細看了。主要看下對 TinkerDexLoader, TinkerResourceLoaderTinkerSoLoader 的調用。

TinkerLoader 裏搜 TinkerDexLoader

首先看下 TinkerDexLoader 是如何校驗、加載的 dex。

加載 dex

TinkerDexLoader 的兩個方法:

  1. TinkerDexLoader#checkComplete 檢查 dex 補丁文件和優化過的 odex 文件是否可以加載
  2. 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_imagebase.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 步:

  1. 新建一個 AssetManager ,通過 addAssetPath() 方法把補丁資源目錄傳遞進去
  2. 替換所有 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 裏主要包括兩部分內容 :

  1. header,magic 和 version ,記錄各個 Section 的偏移量
  2. 其餘部分是不同類型的變更情況,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

  1. dex 讀取到內存,Dex#loadFrom, TableOfContents#readFrom,將 dex 文件內容,按照 map-list 分到不同的 Section 中

  2. DexPatchGenerator構造函數,初始化了 15 個對不同區域的算法,目的就是前面說的,計算出每個區域的變更情況

  3. DexPatchGenerator#executeAndSaveTo,調用 15 個算法的 execute()simulatePatchOperation()

stringDataSectionDiffAlg 爲例,看下做了什麼。

DexSectionDiffAlgorithm

看下它的 execute()simulatePatchOperation() 方法 。

execute() 做的工作:

  1. 拿到舊、新 dex 的 Section Item 列表,排序
  2. 遍歷,對比每個 item
  3. 做一些排序處理,合併相同 index 的 ADD 和 DEL 爲 REPLACE
  4. 把不同類型的操作,保存到三個 map

經歷完成 execute 之後,我們主要的產物就是 3 個Map,

indexToDelOperationMap,indexToAddOperationMap,indexToReplaceOperationMap

分別記錄了:oldDex 中哪些index需要刪除;newDex中新增了哪些item;哪些item需要替換爲新item。

simulatePatchOperation() 做的工作:根據前面的 3 個 map,計算變更數據 index 和 offset,計算下一個 Section 需要依賴前面的 offset。

經過這兩個方法 ,得到了這個 Section 的 patchOperationListpatchSectionSize

執行完所有算法,就可以得到整個 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

  1. Dex File -> DexPatchFile
  2. 檢查 patch 是否能作用到當前的 oldDex(通過比較 oldDex 簽名和 patch 裏記錄的 oldDex 簽名,簽名使用 SHA-1 算法)
  3. 把 patch 裏記錄的合併後的各個 Section 的值複製給合併後 dex 的 TableOfContents
  4. 創建 15 個合併算法處理器,處理不同區域的數據合併
  5. 最後,寫入 header mapList 和 合併後 dex 的簽名和校驗和

每個 Section 的合併算法類似,繼承自 DexSectionPatchAlgorithm:

  1. 讀取保存 del add replace 操作 index 的數組
  2. 計算出合併後的 itemCount: newItemCount = oldItemCount - deleteItemCount + addItemCount
  3. 往合併後的 dex 對應的 xxData 區域寫最終內容(包括沒變的、新增的和替換的)

從 0 開始,按順序寫合併後的內容規則:

  1. 首看先這個位置是否有新增的
  2. 然後看這個位置是否需要替換爲 newItem
  3. 如果這個位置包含在 del/replace 數組裏,那就跳過這個位置 oldItem
  4. 否則說明這個位置的 oldItem 元素沒動,寫到合併後的 dex 裏

Thanks

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章