1. 程式人生 > >Android熱更新開源專案Tinker原始碼解析系列之一:Dex熱更新

Android熱更新開源專案Tinker原始碼解析系列之一:Dex熱更新

Tinker是微信的第一個開源專案,主要用於安卓應用bug的熱修復和功能的迭代。

Tinker github地址:https://github.com/Tencent/tinker

首先向微信致敬,感謝毫無保留的開源出了這麼一款優秀的熱更新專案。

因Tinker支援Dex,資原始檔及so檔案的熱更新,本系列將從以下三個方面對Tinker進行原始碼解析:

Tinker中Dex的熱更新也主要分為三個部分,本文也將從這三個方面進行分析:

  1. 生成補丁流程
  2. 補丁包下發成功後合成全量Dex流程
  3. 生成全量Dex後的載入流程

一、生成補丁流程

當在命令列裡面呼叫tinkerPatchRelease任務時會呼叫com.tencent.tinker.build.patch.Runner.tinkerPatch()進行生成補丁生成過程。

複製程式碼
 1 //gen patch
 2 ApkDecoder decoder = new ApkDecoder(config);
 3 decoder.onAllPatchesStart();
 4 decoder.patch(config.mOldApkFile, config.mNewApkFile);
 5 decoder.onAllPatchesEnd();
 6 
 7 //gen meta file and version file
 8 PatchInfo info = new PatchInfo(config);
 9 info.gen();
10 
11 //build patch
12 PatchBuilder builder = new PatchBuilder(config); 13 builder.buildPatch();
複製程式碼

ApkDecoder.patch(File oldFile, File newFile)函式中,

會先對manifest檔案進行檢測,看其是否有更改,如果發現manifest的元件有新增,則丟擲異常,因為目前Tinker暫不支援四大元件的新增。

檢測通過後解壓apk檔案,遍歷新舊apk,交給ApkFilesVisitor進行處理。

複製程式碼
1 //check manifest change first
2 manifestDecoder.patch(oldFile, newFile);
3 4 unzipApkFiles(oldFile, newFile); 5 6 Files.walkFileTree(mNewApkDir.toPath(), new ApkFilesVisitor(config, mNewApkDir.toPath(), mOldApkDir.toPath(), dexPatchDecoder, soPatchDecoder, resPatchDecoder));
複製程式碼

ApkFilesVisitor的visitFile函式中,對於dex型別的檔案,呼叫dexDecoder進行patch操作;

對於so型別的檔案,使用soDecoder進行patch操作;

對於Res型別檔案,使用resDecoder進行操作。

本文中主要是針對dexDecoder進行分析。

複製程式碼
 1 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
 2 
 3     Path relativePath = newApkPath.relativize(file);
 4 
 5     Path oldPath = oldApkPath.resolve(relativePath);
 6 
 7     File oldFile = null;
 8     //is a new file?!
 9     if (oldPath.toFile().exists()) {
10         oldFile = oldPath.toFile();
11     }
12     String patternKey = relativePath.toString().replace("\\", "/");
13 
14     if (Utils.checkFileInPattern(config.mDexFilePattern, patternKey)) {
15         //also treat duplicate file as unchanged
16         if (Utils.checkFileInPattern(config.mResFilePattern, patternKey) && oldFile != null) {
17             resDuplicateFiles.add(oldFile);
18         }
19 
20         try {
21             dexDecoder.patch(oldFile, file.toFile());
22         } catch (Exception e) {
23 //                    e.printStackTrace();
24             throw new RuntimeException(e);
25         }
26         return FileVisitResult.CONTINUE;
27     }
28     if (Utils.checkFileInPattern(config.mSoFilePattern, patternKey)) {
29         //also treat duplicate file as unchanged
30         if (Utils.checkFileInPattern(config.mResFilePattern, patternKey) && oldFile != null) {
31             resDuplicateFiles.add(oldFile);
32         }
33         try {
34             soDecoder.patch(oldFile, file.toFile());
35         } catch (Exception e) {
36 //                    e.printStackTrace();
37             throw new RuntimeException(e);
38         }
39         return FileVisitResult.CONTINUE;
40     }
41     if (Utils.checkFileInPattern(config.mResFilePattern, patternKey)) {
42         try {
43             resDecoder.patch(oldFile, file.toFile());
44         } catch (Exception e) {
45 //                    e.printStackTrace();
46             throw new RuntimeException(e);
47         }
48         return FileVisitResult.CONTINUE;
49     }
50     return FileVisitResult.CONTINUE;
複製程式碼

DexDiffDecoder.patch(final File oldFile, final File newFile)
首先檢測輸入的dex檔案中是否有不允許修改的類被修改了,如loader相關的類是不允許被修改的,這種情況下會丟擲異常;

如果dex是新增的,直接將該dex拷貝到結果檔案;

如果dex是修改的,收集增加和刪除的class。oldAndNewDexFilePairList將新舊dex對應關係儲存起來,用於後面的分析。

複製程式碼
 1 excludedClassModifiedChecker.checkIfExcludedClassWasModifiedInNewDex(oldFile, newFile);
 2 ...
 3 //new add file
 4 if (oldFile == null || !oldFile.exists() || oldFile.length() == 0) {
 5     hasDexChanged = true;
 6     if (!config.mUsePreGeneratedPatchDex) {
 7         copyNewDexAndLogToDexMeta(newFile, newMd5, dexDiffOut);
 8         return true;
 9     }
10 }
11 ...
12 // collect current old dex file and corresponding new dex file for further processing.
13 oldAndNewDexFilePairList.add(new AbstractMap.SimpleEntry<>(oldFile, newFile));
複製程式碼

UniqueDexDiffDecoder.patch中將新的dex檔案加入到addedDexFiles。

複製程式碼
 1 public boolean patch(File oldFile, File newFile) throws IOException, TinkerPatchException {
 2     boolean added = super.patch(oldFile, newFile);
 3     if (added) {
 4         String name = newFile.getName();
 5         if (addedDexFiles.contains(name)) {
 6             throw new TinkerPatchException("illegal dex name, dex name should be unique, dex:" + name);
 7         } else {
 8             addedDexFiles.add(name);
 9         }
10     }
11     return added;
12 }
複製程式碼

在patch完成後,會呼叫generatePatchInfoFile生成補丁檔案。
DexFiffDecoder.generatePatchInfoFile中首先遍歷oldAndNewDexFilePairList,取出新舊檔案對。

判斷新舊檔案的MD5是否相等,不相等,說明有變化,會根據新舊檔案建立DexPatchGenerator,

DexPatchGenerator建構函式中包含了15個Dex區域的比較演算法:

  • StringDataSectionDiffAlgorithm
  • TypeIdSectionDiffAlgorithm
  • ProtoIdSectionDiffAlgorithm
  • FieldIdSectionDiffAlgorithm
  • MethodIdSectionDiffAlgorithm
  • ClassDefSectionDiffAlgorithm
  • TypeListSectionDiffAlgorithm
  • AnnotationSetRefListSectionDiffAlgorithm
  • AnnotationSetSectionDiffAlgorithm
  • ClassDataSectionDiffAlgorithm
  • CodeSectionDiffAlgorithm
  • DebugInfoItemSectionDiffAlgorithm
  • AnnotationSectionDiffAlgorithm
  • StaticValueSectionDiffAlgorithm
  • AnnotationsDirectorySectionDiffAlgorithm

DexDiffDecoder.executeAndSaveTo(OutputStream out) 這個函式裡面會根據上面的15個演算法對dex的各個區域進行比較,最後生成dex檔案的差異,

這是整個dex diff演算法的核心以StringDataSectionDiffAlgorithm為例,演算法流程如下:

--------------------------------------------

獲取oldDex中StringData區域的Item,並進行排序
獲取newDex中StringData區域的Item,並進行排序
然後對ITEM依次比較
<0
 說明從老的dex中刪除了該String,patchOperationList中新增Del操作
\>0
 說明添加了該String,patchOperationList新增add操作
=0
 說明都有該String, 記錄oldIndexToNewIndexMap,oldOffsetToNewOffsetMap
old item已到結尾
 剩下的item說明都是新增項,patchOperationList新增add操作
new item已到結尾
 剩下的item說明都是刪除項,patchOperationList新增del操作
最後對對patchOperationList進行優化(
{OP_DEL idx} followed by {OP_ADD the_same_idx newItem} will be replaced by {OP_REPLACE idx newItem})

--------------------------------------------

Dexdiff得到的最終生成產物就是針對原dex的一個操作序列。
關於DexDiff演算法,更加詳細的介紹可以參考https://www.zybuluo.com/dodola/note/554061,演算法名曰二路歸併。

對每個區域比較後會將比較的結果寫入檔案中,檔案格式寫在DexDataBuffer中

複製程式碼
 1  private void writeResultToStream(OutputStream os) throws IOException {
 2     DexDataBuffer buffer = new DexDataBuffer();
 3     buffer.write(DexPatchFile.MAGIC);
 4     buffer.writeShort(DexPatchFile.CURRENT_VERSION);
 5     buffer.writeInt(this.patchedDexSize);
 6     // we will return here to write firstChunkOffset later.
 7     int posOfFirstChunkOffsetField = buffer.position();
 8     buffer.writeInt(0);
 9     buffer.writeInt(this.patchedStringIdsOffset);
10     buffer.writeInt(this.patchedTypeIdsOffset);
11     buffer.writeInt(this.patchedProtoIdsOffset);
12     buffer.writeInt(this.patchedFieldIdsOffset);
13     buffer.writeInt(this.patchedMethodIdsOffset);
14     buffer.writeInt(this.patchedClassDefsOffset);
15     buffer.writeInt(this.patchedMapListOffset);
16     buffer.writeInt(this.patchedTypeListsOffset);
17     buffer.writeInt(this.patchedAnnotationSetRefListItemsOffset);
18     buffer.writeInt(this.patchedAnnotationSetItemsOffset);
19     buffer.writeInt(this.patchedClassDataItemsOffset);
20     buffer.writeInt(this.patchedCodeItemsOffset);
21     buffer.writeInt(this.patchedStringDataItemsOffset);
22     buffer.writeInt(this.patchedDebugInfoItemsOffset);
23     buffer.writeInt(this.patchedAnnotationItemsOffset);
24     buffer.writeInt(this.patchedEncodedArrayItemsOffset);
25     buffer.writeInt(this.patchedAnnotationsDirectoryItemsOffset);
26     buffer.write(this.oldDex.computeSignature(false));
27     int firstChunkOffset = buffer.position();
28     buffer.position(posOfFirstChunkOffsetField);
29     buffer.writeInt(firstChunkOffset);
30     buffer.position(firstChunkOffset);
31 
32     writePatchOperations(buffer, this.stringDataSectionDiffAlg.getPatchOperationList());
33     writePatchOperations(buffer, this.typeIdSectionDiffAlg.getPatchOperationList());
34     writePatchOperations(buffer, this.typeListSectionDiffAlg.getPatchOperationList());
35     writePatchOperations(buffer, this.protoIdSectionDiffAlg.getPatchOperationList());
36     writePatchOperations(buffer, this.fieldIdSectionDiffAlg.getPatchOperationList());
37     writePatchOperations(buffer, this.methodIdSectionDiffAlg.getPatchOperationList());
38     writePatchOperations(buffer, this.annotationSectionDiffAlg.getPatchOperationList());
39     writePatchOperations(buffer, this.annotationSetSectionDiffAlg.getPatchOperationList());
40     writePatchOperations(buffer, this.annotationSetRefListSectionDiffAlg.getPatchOperationList());
41     writePatchOperations(buffer, this.annotationsDirectorySectionDiffAlg.getPatchOperationList());
42     writePatchOperations(buffer, this.debugInfoSectionDiffAlg.getPatchOperationList());
43     writePatchOperations(buffer, this.codeSectionDiffAlg.getPatchOperationList());
44     writePatchOperations(buffer, this.classDataSectionDiffAlg.getPatchOperationList());
45     writePatchOperations(buffer, this.encodedArraySectionDiffAlg.getPatchOperationList());
46     writePatchOperations(buffer, this.classDefSectionDiffAlg.getPatchOperationList());
47 
48     byte[] bufferData = buffer.array();
49     os.write(bufferData);
50     os.flush();
51 }
複製程式碼

生成的檔案以dex結尾,但需要注意的是,它不是真正的dex檔案,其格式可參考DexDataBuffer類。

二、補丁包下發成功後合成全量Dex流程

當app收到伺服器下發的補丁後,會觸發DefaultPatchListener.onPatchReceived事件,

呼叫TinkerPatchService.runPatchService啟動patch程序進行補丁patch工作。

UpgradePatch.tryPatch()中會首先檢查補丁的合法性,簽名,以及是否安裝過補丁,檢查通過後會嘗試dex,so以及res檔案的patch。

本文中主要分析DexDiffPatchInternal.tryRecoverDexFiles,討論dex的patch過程。

1 DexDiffPatchInternal.tryRecoverDexFiles
2 BsDiffPatchInternal.tryRecoverLibraryFiles
3 ResDiffPatchInternal.tryRecoverResourceFiles
4 rewritePatchInfoFileWithLock

tryRecoverDexFiles呼叫DexDiffPatchInternal.patchDexFile,

最終通過DexPatchApplier.executeAndSaveTo進行執行及生產全量dex。

複製程式碼

相關推薦

Android更新開源專案Tinker原始碼解析系列之一Dex更新

Tinker是微信的第一個開源專案,主要用於安卓應用bug的熱修復和功能的迭代。 Tinker github地址:https://github.com/Tencent/tinker 首先向微信致敬,感謝毫無保留的開源出了這麼一款優秀的熱更新專案。

Python優秀開源專案Rich原始碼解析

這篇文章對優秀的開源專案Rich的原始碼進行解析,OMG,盤他。為什麼建議閱讀原始碼,有兩個原因,第一,單純學語言很難在實踐中靈活應用,通過閱讀原始碼可以看到每個知識點的運用場景,印象會更深,以後寫程式碼的時候就能應用起來;第二,通過閱讀優秀的開原始碼,可以學習比人的程式碼規範、設計思路;第三,參與到開源社群

android面試——開源框架的原始碼解析

1、EventBus (1)通過註解+反射來進行方法的獲取 註解的使用:@Retention(RetentionPolicy.RUNTIME)表示此註解在執行期可知,否則使用CLASS或者SOURCE在執行期間會被丟棄。 通過反射來獲取類和方法:因為對映關係實際上是類對映到所有此類

Android 常用開源框架源碼解析 系列 (十)Rxjava 異步框架

oid super 嚴重 ids 代碼 執行者 輸出 ... tin 一、Rxjava的產生背景 一、進行耗時任務 傳統解決辦法: 傳統手動開啟子線程,聽過接口回調的方式獲取結果 傳統解決辦法的缺陷: 隨著項目的深入、擴展。代碼量

Android 常用開源框架源碼解析 系列 (十一)picasso 圖片框架

hand 需求 trim cor pan setname github ESS true 一、前言 Picasso 強大的圖片加載緩存框架 api加載方式和Glide 類似,均是通過鏈式調用的方式進行調用 1.1、作用 Picasso 管理整個圖片加載、轉換、緩存

Android 常用開源框架源碼解析 系列 (九)dagger2 呆哥兔 依賴註入庫

ica 記得 接口 手動 識別 pda 進行 strace 內聚 一、前言 依賴註入定義 目標類中所依賴的其他的類的初始化過程,不是通過手動編碼的方式創建的。 是將其他的類已經初始化好的實例自動註入的目標類中。 “依賴註入”也是面向對象編程的 設計模式 ————

【機器人學】機器人開源專案KDL原始碼學習(8)KDL的精髓

  首先說一下我的心得: 1. 我認為KDL的精髓是Spatial Vector,結合C++等面向物件的語言可以寫出較好的軟體。 2. 直接閱讀KDL程式碼不適合初學者學習機械臂動力學。 3. 要學習機械臂動力學的話應首先閱讀使用3維向量推導公式的文獻,也就是線速度和角速度獨立分析

【機器人學】機器人開源專案KDL原始碼學習(4)機械臂逆動力學的牛頓尤拉演算法

  機械臂的逆動力學問題可以認為是:已知機械臂各個連桿的關節的運動(關節位移、關節速度和關節加速度),求產生這個加速度響應所需要的力/力矩。KDL提供了兩個求解逆動力學的求解器,其中一個是牛頓尤拉法,這個方法是最簡單和高效的方法。    牛頓尤拉法演算法可以分為三個步驟: step1:

【機器人學】機器人開源專案KDL原始碼學習(6)笛卡爾空間軌跡規劃、圓弧過渡、姿態插值、梯形速度、pathlength

  本文的內容是對另一篇文章(連結)的補充,對Trajectory_example.cpp涉及到的原理作一些簡單的講解,主要內容是:   (1)機器人路徑規劃圓弧過渡的原理;   (2)機器人路徑規劃梯形波的原理;   (3)機器人末端姿態插值的方法(角-軸);   (4)KDL

【機器人學】機器人開源專案KDL原始碼學習(7)examples中的CMakeList.txt檔案解讀

通過學習KDL開源專案的程式碼可以學習CMake構建程式的知識,現簡單介紹一下orocos_kinematics_dynamics-master\orocos_kinematics_dynamics-master\orocos_kdl\examples\CMakeList.txt檔案的指令。

【機器人學】機器人開源專案KDL原始碼學習(5)KDL如何求解幾何雅克比矩陣

這篇文章試圖說清楚兩件事:1. 幾何雅克比矩陣的本質;2. KDL如何求解機械臂的幾何雅克比矩陣。 一、幾何雅克比矩陣的本質 機械臂的關節空間的速度可以對映到執行器末端在操作空間的速度,這種對映可以通過一個矩陣來描述,就是幾何雅克比矩陣,瞭解雅克比矩陣需要了解這種對映關係的本質,這

【機器人學】機器人開源專案KDL原始碼學習(3)機器人操作空間路徑規劃(Path Planning)和軌跡規劃(Trajectory Planning)示例

很多同學會把路徑規劃(Path Planning)和軌跡規劃(Trajectory Planning)這兩個概念混淆,路徑規劃只是表示了機械臂末端在操作空間中的幾何資訊,比如從工作臺的一端(A點)沿直線移動到另一端(B點)。而軌跡規劃則加上了時間律,比如它要完成的任務是從A點開始到B點結束,中間

【轉】如何閱讀大型前端開源專案原始碼

目前網上有很多「XX原始碼分析」這樣的文章,不過這些文章分析原始碼的範圍有限,有時候講的內容不是讀者最關心的。同時我也注意到,原始碼是在不斷更新的,文章裡寫的原始碼往往已經過時了。因為這些問題,很多同學都喜歡自己看原始碼,自己動手,豐衣足食。 這篇文章主要講的是閱讀大型的前

Vue-cli 命令列工具專案配置原始碼解析

我們都知道vue cli是vue.js 提供一個官方搭建專案命令列工具,可用於快速搭建大型單頁應用。以下是Vue-cli @2.9搭建專案的配置原始碼解析 相關目錄 ├── build │ ├── build.js │ ├── utils.js │ ├── check-versions

Android應用Context詳解及原始碼解析

轉自 http://blog.csdn.net/yanbober/article/details/45967639  1  背景 今天突然想起之前在上家公司(做TV與BOX盒子)時有好幾個人問過我關於Android的Context到底是啥的問題,所以就馬上

向大神看齊: 如何閱讀大型前端開源專案原始碼(轉)

作者簡介 Daniel 螞蟻金服·資料體驗技術團隊轉自: https://github.com/ProtoTeam/blog/blob/master/201805/3.md目前網上有很多「XX原始碼分析」這樣的文章,不過這些文章分析原始碼的範圍有限,有時候講的內容不是讀者最關

Android 網路程式設計(7): 原始碼解析OkHttp前篇[請求網路]

前言 學會了OkHttp3的用法後,我們當然有必要來了解下OkHttp3的原始碼,當然現在網上的文章很多,我仍舊希望我這一系列文章篇是最簡潔易懂的。 1.從請求處理開始分析 首先OKHttp3如何使用這裡就不在贅述了,不明白的同學可以檢視Android網路程式設計(5):Ok

Android 網路程式設計(8): 原始碼解析 OkHttp 中篇[複用連線池]

1.引子 在瞭解OkHttp的複用連線池之前,我們首先要了解幾個概念。 TCP三次握手 通常我們進行HTTP連線網路的時候我們會進行TCP的三次握手,然後傳輸資料,然後再釋放連線。 TCP三次握手的過程為: 第一次握手:建立連線。客戶端傳送連線請求報文段,將SYN位置為1

《Linux高效能伺服器》附帶專案springsnil原始碼解析

原始碼地址 安裝及使用 下載原始碼: git clone https://github.com/liu-jianhao/springsnail.git 然後進入springsnil目錄直接make即可生成可執行檔案 填寫配置檔案,我測試的是網站是網易雲音樂,首先我先看看網

Android 收集的開源專案和文章集合

2017-12-212.炫酷開屏動畫框架3.防京東,支付寶密碼鍵盤和密碼輸入框4.根據銀行卡號 獲取 銀行卡型別、銀行名稱和銀行編碼 自動格式化銀行卡號、手機號、身份證號輸入的工具類5.高仿 iOS 滾輪實現 省市區 城市選擇三級聯動6.驗證碼輸入框控制元件.7.輕量級提示框,載入中、成功、失敗、警告等,以及