1. 程式人生 > >Android熱更新技術的研究與實現(三)

Android熱更新技術的研究與實現(三)

微信 Tinker

看完上面的Qzone超級補丁方案,我們不禁會想有沒有那麼一種方案,能做到開發透明,但是卻沒有QZone方案的缺陷呢?肯定是有的,比如我們完全可以使用新的Dex,那樣既不出現Art地址錯亂的問題,在Dalvik也無須插樁。當然考慮到補丁包的體積,我們不能直接將新的Dex放在裡面。但我們可以將新舊兩個Dex的差異放到補丁包中,最簡單我們可以採用BsDiff演算法。

Tinker是微信官方的Android熱補丁解決方案,它支援動態下發程式碼、So庫以及資源,讓應用能夠在不需要重新安裝的情況下實現更新。

Tinker和以往的HotFix庫思路不太一樣,它更像是APP的增量更新,在伺服器端通過差異性演算法,計算出新舊dex之間的差異包,推送到客戶端,進行合成。傳統的差異性演算法有BsDiff,而Tinker的牛逼之處就在於它自己基於Dex的檔案格式,研發出了DexDiff演算法,這個我們後面再說。

如果我們的應用想要整合Tinker熱更新的話,可以直接在騰訊的Bugly建立自己的應用,然後接入。這裡我就建立了一個應用,但是整合我是直接使用官方的例子。因為官方給出的整合步驟很詳細,還有對應的一整套教程,大家用起來應該都很方便。

首先建立一個應用,獲取AppID和AppKey,然後在GitHub上下載BuglyHotfixEasyDemo,目錄結構如下:

Bugly建立應用

官方BuglyDemo

BugClass就是存在錯誤的類:

public class BugClass {

    public String bug() {
        // 這段程式碼會報空指標異常
        // String str = null;
        // Log.e("BugClass", "get string length:" + str.length());
        return "This is a bug class";
    }
}

LoadBugClass就是獲取BugClass中返回的字串

public class LoadBugClass {

    /**
     *獲取bug字串.
     *
     *@return 返回bug字串
     */
    public static String getBugString() {
        BugClass bugClass = new BugClass();
        return bugClass.bug();
    }
}

而MainActivity中有很多按鈕,其中有一個按鈕式,點選彈出Toast,顯示的內容就是上面返回的字串;

 /**********省略N行程式碼*************/ 
 /**
 *根據應用patch包前後來測試是否應用patch包成功.
 *
 *應用patch包前,提示"This is a bug class"
 *應用patch包之後,提示"The bug has fixed"
 */
public void testToast() {
    Toast.makeText(this, LoadBugClass.getBugString(), Toast.LENGTH_SHORT).show();
}

@Override
public void onClick(View v) {
    switch (v.getId()) {
        case R.id.btnShowToast:  // 測試熱更新功能  點選顯示結果按鈕
            testToast();
            break;
/***********再次省略N行程式碼************/

從專案結構上看也是很簡單那的一個例子,多渠道打包我們就不嘗試了,就來個簡單的基本打包實現吧!

顯示效果(點選顯示效果按鈕後,現在還是有bug的包,所以顯示的是bug class):

1、編譯基準包

配置基準包的tinkerId

在配置好如AppId等之後還需要在tinker-support.gradle檔案中需要寫入自己的配置:

inkerId最好是一個唯一標識,例如git版本號、versionName等等。 如果你要測試熱更新,你需要對基線版本進行聯網上報。

這裡強調一下,基線版本配置一個唯一的tinkerId,而這個基線版本能夠應用補丁的前提是整合過熱更新SDK,並啟動上報過聯網,這樣我們後臺會將這個tinkerId對應到一個目標版本,例如tinkerId = “bugly_1.0.0” 對應了一個目標版本是1.0.0,基於這個版本打的補丁包就能匹配到目標版本。

編譯生成基準包(原包,含bug)

執行assembleRelease編譯生成基準包:

會在build/baseApk目錄下生成如下檔案,具體路徑和檔名可以自己配置

啟動apk,上報聯網資料
我們每次冷啟動都會請求補丁策略,會上報當前版本號和tinkerId,這樣我們後臺就能將這個唯一的tinkerId對應到一個版本,測試的時候可以開啟logcat檢視我們的日誌,如下圖所示:

我們能看到tinkerId;

2、對基線版本的bug修復

其實就是講BugClass中的返回字串改為“The bug has fixed”;

3、根據基線版本生成補丁包

修改待修復apk路徑、mapping檔案路徑、resId檔案路徑

 /**
  *此處填寫每次構建生成的基準包目錄 
  */
 def baseApkDir = "app-0813-20-54-50" //改成剛才生成的目錄  其實是按日期時間生成的目錄
 tinkerId = "1.0.1-patch"

執行構建補丁包的task,其實生成的就是bug修復的完整apk

如果你要生成不同編譯環境的補丁包,只需要執行TinkerSupport外掛生成的task,比如buildTinkerPatchRelease就能生成release編譯環境的補丁包。 注:TinkerSupport外掛版本低於1.0.4的,需要使用tinkerPatchRelease來生成補丁包 。

生成的補丁包在build/outputs/patch目錄下:

主要會生成3個檔案:unSignedApk,signedApk以及signedWith7ZipApk。

unSignedApk只要將tinker_result中的檔案壓縮到一個壓縮包即可。
signedApk將unSignedApk使用jarsigner進行簽名。

signedWith7ZipApk主要是對signedApk進行解壓再做sevenZip壓縮。

4、上傳補丁包到平臺

見證奇蹟的時刻到了!!上傳補丁包到平臺並下發編輯規則,點擊發布新補丁,上傳前面生成的patch包,平臺會自動為你匹配到目標版本,可以選擇下發範圍(開發裝置、全量裝置、自定義),填寫完備註之後,點選立即下發讓補丁生效,這樣你就可以在客戶端當中收到我們的策略,SDK會自動幫你把補丁包下到本地。

再次啟動會發現停止執行,那是因為客戶端收到策略需要下載補丁更新,最後的修復後效果:

好的,這下Bugly熱更新我們就簡單的看了下效果,其所應用的就是微信的Tinker方案,其實不難看出,Bugly和阿里的Sophix都是針對補丁包的一種下發策略。

對於微信來說,實現熱更新使用一個“高可用”的補丁框架,至少滿足以下幾個條件:

  1. 穩定性與相容性;微信需要在數億臺裝置上執行,即使補丁框架帶來1%的異常,也將影響到數萬使用者。保證補丁框架的穩定性與相容性是我們的第一要務;

  2. 效能;微信對效能要求也非常苛刻,首先補丁框架不能影響應用的效能,這裡基於大部分情況下使用者不會使用到補丁。其次補丁包應該儘量少,這關係到使用者流量與補丁的成功率問題;

  3. 易用性;在解決完以上兩個核心問題的前提下,我們希望補丁框架簡單易用,並且可以全面支援,甚至可以做到功能釋出級別。

而熱更新技術的兩大流派,一種就是阿里的Native流派,即AndFix和Sophix,還有一種就是騰訊自己的Qzone超級補丁屬於java流派,最後微信還是選擇了繼續走自己的java流派(自己的路就是要一走到黑!),但是微信並不是固守陳規,而是追求極致!這不得不提到文章前面提到的DexDiff演算法了。

我們都知道dex檔案是執行在Dalvik中的位元組碼檔案,類似於運行於JVM中的class檔案,在反編譯的時候,apk中會包含一個或者多個*.dex檔案,該檔案中儲存了我們編寫的程式碼,一般情況下我們還會通過工具轉化為jar,然後通過一些工具反編譯檢視(dex2jar)。

jar檔案大家應該都清楚,類似於class檔案的壓縮包,一般情況下,我們直接解壓就可以看到一個個class檔案。而dex檔案我們無法通過解壓獲取內部的一class檔案,那肯定是因為它的格式決定的,具體的格式我們不在這裡分析,我們看一下DexDiff的基本步驟(細節暫不考慮):

  1. 首先,明確有這麼幾個東西,bugdex,bugfixeddex,patchdex;

  2. 其次,計算出bugfixeddex中每一部分(指的是dex結構中的某一特定部分)佔用的大小;

  3. 然後,比較bugdex和bugfixeddex的每一部分,對每一部分進行對比,並記錄不同(刪除了哪些,新增了哪些,記錄和儲存以什麼形式我們暫時不管)。

  4. 最後,將儲存的不同的記錄寫入補丁中

Tinker中Dex的熱更新主要分為三個部分: 一、補丁包的生成; 二、補丁包下發後生成全量Dex; 三、生成全量Dex後的載入過程。

我們昨天在生成補丁的時候,呼叫了tinker-support中的buildTinkerPatchRelease

當我們執行這個之後,
執行時間最長的當屬tinkerPatchRelease的這個過程,

那麼具體的Tinker是如何實現熱更新的呢?原始碼出真知,我們下載tinker的原始碼來看看不就知道了嘛,畢竟是開源的嘛!“tinker原始碼傳送”

我下載的是目前最新的1.8.1版本。原始碼我們挑重點看,目錄結構如下:

tinker原始碼 tinker-patch-lib

一、補丁包的生成;

com.tencent.tinker.build.patch.Runner這個類就是我們在執行buildTinkerPatchRelease會執行的類,具體是執行類中的tinkerPatch()方法:

 protected void tinkerPatch() {
    Logger.d("-----------------------Tinker patch begin-----------------------");

    Logger.d(config.toString());
    try {
        //gen patch
        ApkDecoder decoder = new ApkDecoder(config);
        decoder.onAllPatchesStart();
        decoder.patch(config.mOldApkFile, config.mNewApkFile);
        decoder.onAllPatchesEnd();

        //gen meta file and version file
        PatchInfo info = new PatchInfo(config);
        info.gen();

        //build patch
        PatchBuilder builder = new PatchBuilder(config);
        builder.buildPatch();

    } catch (Throwable e) {
        e.printStackTrace();
        goToError();
    }

    Logger.d("Tinker patch done, total time cost: %fs", diffTimeFromBegin());
    Logger.d("Tinker patch done, you can go to file to find the output %s", config.mOutFolder);
    Logger.d("-----------------------Tinker patch end-------------------------");
}

這個其實就是生成補丁的過程,其中呼叫com.tencent.tinker.build.decoder.ApkDecoder中patch(File oldFile, File newFile)方法:

 public boolean patch(File oldFile, File newFile) throws Exception {
    writeToLogFile(oldFile, newFile);
    //check manifest change first
    manifestDecoder.patch(oldFile, newFile);

    unzipApkFiles(oldFile, newFile);

    Files.walkFileTree(mNewApkDir.toPath(), new ApkFilesVisitor(config, mNewApkDir.toPath(), mOldApkDir.toPath(), dexPatchDecoder, soPatchDecoder, resPatchDecoder));

    //get all duplicate resource file
    for (File duplicateRes : resDuplicateFiles) {
    //            resPatchDecoder.patch(duplicateRes, null);
        Logger.e("Warning: res file %s is also match at dex or library pattern, " + "we treat it as unchanged in the new resource_out.zip", getRelativePathStringToOldFile(duplicateRes));
    }

    soPatchDecoder.onAllPatchesEnd();
    dexPatchDecoder.onAllPatchesEnd();
    manifestDecoder.onAllPatchesEnd();
    resPatchDecoder.onAllPatchesEnd();

    //clean resources
    dexPatchDecoder.clean();
    soPatchDecoder.clean();
    resPatchDecoder.clean();
    return true;
}

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

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

ApkFilesVisitor的visitFile方法中,對於dex型別的檔案,呼叫dexDecoder進行patch操作;我們主要是針對dexDecoder進行分析,所以省略so型別和res型別操作程式碼:

  @Override
  public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {

        Path relativePath = newApkPath.relativize(file);

        Path oldPath = oldApkPath.resolve(relativePath);

        File oldFile = null;
        //is a new file?!
        if (oldPath.toFile().exists()) {
            oldFile = oldPath.toFile();
        }
        String patternKey = relativePath.toString().replace("\\", "/");

        if (Utils.checkFileInPattern(config.mDexFilePattern, patternKey)) {
            //also treat duplicate file as unchanged
            if (Utils.checkFileInPattern(config.mResFilePattern, patternKey) && oldFile != null) {
                resDuplicateFiles.add(oldFile);
            }

            try {
                dexDecoder.patch(oldFile, file.toFile());
            } catch (Exception e) {
        //      e.printStackTrace();
                throw new RuntimeException(e);
            }
            return FileVisitResult.CONTINUE;
        }
        if (Utils.checkFileInPattern(config.mSoFilePattern, patternKey)) {
            //also treat duplicate file as unchanged
            /*****省略so解析,對於so型別的檔案,使用soDecoder進行patch操作**************/
        }
        if (Utils.checkFileInPattern(config.mResFilePattern, patternKey)) {
           /*****省略so解析,對於Res型別檔案,使用resDecoder進行操作patch操作**************/
        }
        return FileVisitResult.CONTINUE;
    }

可以看出是呼叫DexDiffDecoder.patch(final File oldFile, final File newFile)方法,原始碼如下:

@SuppressWarnings("NewApi")
@Override
public boolean patch(final File oldFile, final File newFile) throws IOException, TinkerPatchException {
    final String dexName = getRelativeDexName(oldFile, newFile);
    />>>>>>>>>>>>>>>>>>>>>>省略N行程式碼<<<<<<<<<<<<<<<<<<<<<</
    try {
        excludedClassModifiedChecker.checkIfExcludedClassWasModifiedInNewDex(oldFile, newFile);
    } 
    />>>>>>>>>>>>>>>>>>>>>>省略N行程式碼<<<<<<<<<<<<<<<<<<<<<</

    // If corresponding new dex was completely deleted, just return false.
    // don't process 0 length dex
    if (newFile == null || !newFile.exists() || newFile.length() == 0) {
        return false;
    }

    File dexDiffOut = getOutputPath(newFile).toFile();

    final String newMd5 = getRawOrWrappedDexMD5(newFile);

    //new add file
    if (oldFile == null || !oldFile.exists() || oldFile.length() == 0) {
        hasDexChanged = true;
        copyNewDexAndLogToDexMeta(newFile, newMd5, dexDiffOut);
        return true;
    }

    />>>>>>>>>>>>>>>>>>>>>>省略N行程式碼<<<<<<<<<<<<<<<<<<<<<</

    RelatedInfo relatedInfo = new RelatedInfo();
    relatedInfo.oldMd5 = oldMd5;
    relatedInfo.newMd5 = newMd5;

    // collect current old dex file and corresponding new dex file for further processing.
    oldAndNewDexFilePairList.add(new AbstractMap.SimpleEntry<>(oldFile, newFile));

    dexNameToRelatedInfoMap.put(dexName, relatedInfo);

    return true;
}

由原始碼可以看出是先檢測輸入的dex檔案中是否有不允許修改的類被修改了,如loader相關的類是不允許被修改的,這種情況下會丟擲異常;

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

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

單單只是將新的dex檔案加入到addedDexFiles。呼叫的是UniqueDexDiffDecoder.patch:

@Override
public boolean patch(File oldFile, File newFile) throws IOException, TinkerPatchException {
    boolean added = super.patch(oldFile, newFile);
    if (added) {
        String name = newFile.getName();
        if (addedDexFiles.contains(name)) {
            throw new TinkerPatchException("illegal dex name, dex name should be unique, dex:" + name);
        } else {
            addedDexFiles.add(name);
        }
    }
    return added;
}

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

判斷新舊檔案的MD5是否相等,不相等,說明有變化,會根據新舊檔案建立DexPatchGenerator,DexPatchGenerator建構函式中包含了15個Dex區域的比較演算法:

private DexSectionDiffAlgorithm<StringData> stringDataSectionDiffAlg;
private DexSectionDiffAlgorithm<Integer> typeIdSectionDiffAlg;
private DexSectionDiffAlgorithm<ProtoId> protoIdSectionDiffAlg;
private DexSectionDiffAlgorithm<FieldId> fieldIdSectionDiffAlg;
private DexSectionDiffAlgorithm<MethodId> methodIdSectionDiffAlg;
private DexSectionDiffAlgorithm<ClassDef> classDefSectionDiffAlg;
private DexSectionDiffAlgorithm<TypeList> typeListSectionDiffAlg;
private DexSectionDiffAlgorithm<AnnotationSetRefList> annotationSetRefListSectionDiffAlg;
private DexSectionDiffAlgorithm<AnnotationSet> annotationSetSectionDiffAlg;
private DexSectionDiffAlgorithm<ClassData> classDataSectionDiffAlg;
private DexSectionDiffAlgorithm<Code> codeSectionDiffAlg;
private DexSectionDiffAlgorithm<DebugInfoItem> debugInfoSectionDiffAlg;
private DexSectionDiffAlgorithm<Annotation> annotationSectionDiffAlg;
private DexSectionDiffAlgorithm<EncodedValue> encodedArraySectionDiffAlg;
private DexSectionDiffAlgorithm<AnnotationsDirectory> annotationsDirectorySectionDiffAlg;

DexDiffDecoder.executeAndSaveTo(OutputStream out) 這個函式裡面會根據上面的15個演算法對dex的各個區域進行比較,每個演算法代表每個區域,演算法的目的就像我們之前描述DexDiff第3步的那樣,要知道“刪除了哪些,新增了哪些”,最後生成dex檔案的差異。

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

每個演算法都會執行execute和simulatePatchOperation方法:

    /************省略N行程式碼*************/
    this.stringDataSectionDiffAlg.execute();
    this.patchedStringDataItemsOffset = patchedheaderSize + patchedIdSectionSize;
    if (this.oldDex.getTableOfContents().stringDatas.isElementFourByteAligned) {
        this.patchedStringDataItemsOffset
                = SizeOf.roundToTimesOfFour(this.patchedStringDataItemsOffset);
    }
    this.stringDataSectionDiffAlg.simulatePatchOperation(this.patchedStringDataItemsOffset);
    /************省略N行程式碼*************/

首先看execute(程式碼比較長,因為是演算法核心,不好省略,所以分兩部分講下,大家可以去原始碼中看com.tencent.tinker.build.dexpatcher.algorithms.diff.DexSectionDiffAlgorithm)

 public void execute() {
    this.patchOperationList.clear();

    this.adjustedOldIndexedItemsWithOrigOrder = collectSectionItems(this.oldDex, true);
    this.oldItemCount = this.adjustedOldIndexedItemsWithOrigOrder.length;

    AbstractMap.SimpleEntry<Integer, T>[] adjustedOldIndexedItems = new AbstractMap.SimpleEntry[this.oldItemCount];
    System.arraycopy(this.adjustedOldIndexedItemsWithOrigOrder, 0, adjustedOldIndexedItems, 0, this.oldItemCount);
    Arrays.sort(adjustedOldIndexedItems, this.comparatorForItemDiff);

    AbstractMap.SimpleEntry<Integer, T>[] adjustedNewIndexedItems = collectSectionItems(this.newDex, false);
    this.newItemCount = adjustedNewIndexedItems.length;
    Arrays.sort(adjustedNewIndexedItems, this.comparatorForItemDiff);

    int oldCursor = 0;
    int newCursor = 0;
    while (oldCursor < this.oldItemCount || newCursor < this.newItemCount) {
        if (oldCursor >= this.oldItemCount) {
            // rest item are all newItem.
            while (newCursor < this.newItemCount) {
                AbstractMap.SimpleEntry<Integer, T> newIndexedItem = adjustedNewIndexedItems[newCursor++];
                this.patchOperationList.add(new PatchOperation<>(PatchOperation.OP_ADD, newIndexedItem.getKey(), newIndexedItem.getValue()));
            }
        } else
        if (newCursor >= newItemCount) {
            // rest item are all oldItem.
            while (oldCursor < oldItemCount) {
                AbstractMap.SimpleEntry<Integer, T> oldIndexedItem = adjustedOldIndexedItems[oldCursor++];
                int deletedIndex = oldIndexedItem.getKey();
                int deletedOffset = getItemOffsetOrIndex(deletedIndex, oldIndexedItem.getValue());
                this.patchOperationList.add(new PatchOperation<T>(PatchOperation.OP_DEL, deletedIndex));
                markDeletedIndexOrOffset(this.oldToPatchedIndexMap, deletedIndex, deletedOffset);
            }
        } else {
            AbstractMap.SimpleEntry<Integer, T> oldIndexedItem = adjustedOldIndexedItems[oldCursor];
            AbstractMap.SimpleEntry<Integer, T> newIndexedItem = adjustedNewIndexedItems[newCursor];
            int cmpRes = oldIndexedItem.getValue().compareTo(newIndexedItem.getValue());
            if (cmpRes < 0) {
                int deletedIndex = oldIndexedItem.getKey();
                int deletedOffset = getItemOffsetOrIndex(deletedIndex, oldIndexedItem.getValue());
                this.patchOperationList.add(new PatchOperation<T>(PatchOperation.OP_DEL, deletedIndex));
                markDeletedIndexOrOffset(this.oldToPatchedIndexMap, deletedIndex, deletedOffset);
                ++oldCursor;
            } else
            if (cmpRes > 0) {
                this.patchOperationList.add(new PatchOperation<>(PatchOperation.OP_ADD, newIndexedItem.getKey(), newIndexedItem.getValue()));
                ++newCursor;
            } else {
                int oldIndex = oldIndexedItem.getKey();
                int newIndex = newIndexedItem.getKey();
                int oldOffset = getItemOffsetOrIndex(oldIndexedItem.getKey(), oldIndexedItem.getValue());
                int newOffset = getItemOffsetOrIndex(newIndexedItem.getKey(), newIndexedItem.getValue());

                if (oldIndex != newIndex) {
                    this.oldIndexToNewIndexMap.put(oldIndex, newIndex);
                }

                if (oldOffset != newOffset) {
                    this.oldOffsetToNewOffsetMap.put(oldOffset, newOffset);
                }

                ++oldCursor;
                ++newCursor;
            }
        }
      /**********前半部分**********************/
    }

可以看到首先讀取oldDex和newDex對應區域的資料並排序,分別adjustedOldIndexedItems和adjustedNewIndexedItems。
接下來就開始遍歷了,直接看else部分:
分別根據當前的cursor,獲取oldItem和newItem,對其value對對比:
如果<0 ,則認為該old Item被刪除了,記錄為PatchOperation.OP_DEL,並記錄該oldItem index到PatchOperation物件,加入到patchOperationList中。
如果>0,則認為該newItem是新增的,記錄為PatchOperation.OP_ADD,並記錄該newItem index和value到PatchOperation物件,加入到patchOperationList中。
如果=0,不會生成PatchOperation。
經過上述,我們得到了一個patchOperationList物件。
繼續下半部分程式碼:

    /*************後半部分**********************/
    // So far all diff works are done. Then we perform some optimize works.
    // detail: {OP_DEL idx} followed by {OP_ADD the_same_idx newItem}
    // will be replaced by {OP_REPLACE idx newItem}
    Collections.sort(this.patchOperationList, comparatorForPatchOperationOpt);

    Iterator<PatchOperation<T>> patchOperationIt = this.patchOperationList.iterator();
    PatchOperation<T> prevPatchOperation = null;
    while (patchOperationIt.hasNext()) {
        PatchOperation<T> patchOperation = patchOperationIt.next();
        if (prevPatchOperation != null
            && prevPatchOperation.op == PatchOperation.OP_DEL
            && patchOperation.op == PatchOperation.OP_ADD
        ) {
            if (prevPatchOperation.index == patchOperation.index) {
                prevPatchOperation.op = PatchOperation.OP_REPLACE;
                prevPatchOperation.newItem = patchOperation.newItem;
                patchOperationIt.remove();
                prevPatchOperation = null;
            } else {
                prevPatchOperation = patchOperation;
            }
        } else {
            prevPatchOperation = patchOperation;
        }
    }

    // Finally we record some information for the final calculations.
    patchOperationIt = this.patchOperationList.iterator();
    while (patchOperationIt.hasNext()) {
        PatchOperation<T> patchOperation = patchOperationIt.next();
        switch (patchOperation.op) {
            case PatchOperation.OP_DEL: {
                indexToDelOperationMap.put(patchOperation.index, patchOperation);
                break;
            }
            case PatchOperation.OP_ADD: {
                indexToAddOperationMap.put(patchOperation.index, patchOperation);
                break;
            }
            case PatchOperation.OP_REPLACE: {
                indexToReplaceOperationMap.put(patchOperation.index, patchOperation);
                break;
            }
        }
    }
 }

首先對patchOperationList按照index排序,如果index一致則先DEL、後ADD。
接下來一個對所有的operation的迭代,主要將index一致的,且連續的DEL、ADD轉化為REPLACE操作。
最後將patchOperationList轉化為3個Map,分別為:indexToDelOperationMap,indexToAddOperationMap,indexToReplaceOperationMap。
ok,經歷完成execute之後,我們主要的產物就是3個Map,分別記錄了:oldDex中哪些index需要刪除;newDex中新增了哪些item;哪些item需要替換為新item。
這基本上就是DexDif演算法的核心思想了(StringDataSectionDiffAlgorithm舉例,其他的一樣分析);

剛才說了每個演算法除了execute()還有個simulatePatchOperation():

 public void simulatePatchOperation(int baseOffset) {
    boolean isNeedToMakeAlign = getTocSection(this.oldDex).isElementFourByteAligned;
    int oldIndex = 0;
    int patchedIndex = 0;
    int patchedOffset = baseOffset;
    while (oldIndex < this.oldItemCount || patchedIndex < this.newItemCount) {
        if (this.indexToAddOperationMap.containsKey(patchedIndex)) {
            PatchOperation<T> patchOperation = this.indexToAddOperationMap.get(patchedIndex);
            if (isNeedToMakeAlign) {
                patchedOffset = SizeOf.roundToTimesOfFour(patchedOffset);
            }
            T newItem = patchOperation.newItem;
            int itemSize = getItemSize(newItem);
            updateIndexOrOffset(this.newToPatchedIndexMap,0,getItemOffsetOrIndex(patchOperation.index, newItem),0,patchedOffset);
            ++patchedIndex;
            patchedOffset += itemSize;
        } else
        if (this.indexToReplaceOperationMap.containsKey(patchedIndex)) {
            PatchOperation<T> patchOperation = this.indexToReplaceOperationMap.get(patchedIndex);
            /*******省略N程式碼***********/
            ++patchedIndex;
            patchedOffset += itemSize;
        } else
        if (this.indexToDelOperationMap.containsKey(oldIndex)) {
            ++oldIndex;
        } else
        if (this.indexToReplaceOperationMap.containsKey(oldIndex)) {
            ++oldIndex;
        } else
        if (oldIndex < this.oldItemCount) {
            /*******省略N程式碼***********/
            ++oldIndex;
            ++patchedIndex;
            patchedOffset += itemSize;
        }
    }
    this.patchedSectionSize = SizeOf.roundToTimesOfFour(patchedOffset - baseOffset);
}

首先是要遍歷oldIndex與newIndex,分別在indexToAddOperationMap,indexToReplaceOperationMap,indexToDelOperationMap中查詢。
這裡關注一點最終的一個產物是this.patchedSectionSize,由patchedOffset-baseOffset得到。

這裡有幾種情況會造成patchedOffset+=itemSize:

  1. indexToAddOperationMap中包含patchIndex
  2. indexToReplaceOperationMap包含patchIndex
  3. 不在indexToDelOperationMap與indexToReplaceOperationMap中的oldDex.

這個patchedSectionSize其實對應newDex的這個區域的size。所以,包含需要ADD的Item,會被替代的Item,以及OLD ITEMS中沒有被刪除和替代的Item。

這三者相加即為newDex的itemList。
到這裡,StringDataSectionDiffAlgorithm演算法就執行完畢了。

經過這樣的一個演算法,我們得到了PatchOperationList和對應區域sectionSize。那麼執行完成所有的演算法,應該會得到針對每個演算法的PatchOperationList,和每個區域的sectionSize;每個區域的sectionSize實際上換算得到每個區域的offset。

每個區域的演算法,execute和simulatePatchOperation程式碼都是複用的父類 com.tencent.tinker.build.dexpatcher.algorithms.diff.DexSectionDiffAlgorithm 的方法,所以其他的都差不多,可以自己檢視。
接下來看執行完成所有的演算法後的writeResultToStream方法:

 private void writeResultToStream(OutputStream os) throws IOException {
    DexDataBuffer buffer = new DexDataBuffer();
    buffer.write(DexPatchFile.MAGIC);
    buffer.writeShort(DexPatchFile.CURRENT_VERSION);
    buffer.writeInt(this.patchedDexSize);
    // we will return here to write firstChunkOffset later.
    int posOfFirstChunkOffsetField = buffer.position();
    buffer.writeInt(0);
    buffer.writeInt(this.patchedStringIdsOffset);
    buffer.writeInt(this.patchedTypeIdsOffset);
    buffer.writeInt(this.patchedProtoIdsOffset);
    /*****省略其他演算法***********/
    buffer.write(this.oldDex.computeSignature(false));
    int firstChunkOffset = buffer.position();
    buffer.position(posOfFirstChunkOffsetField);
    buffer.writeInt(firstChunkOffset);
    buffer.position(firstChunkOffset);

    writePatchOperations(buffer, this.stringDataSectionDiffAlg.getPatchOperationList());
    writePatchOperations(buffer, this.typeIdSectionDiffAlg.getPatchOperationList());
    writePatchOperations(buffer, this.typeListSectionDiffAlg.getPatchOperationList());
    /*****省略其他演算法***********/

    byte[] bufferData = buffer.array();
    os.write(bufferData);
    os.flush();
}

首先寫了MAGIC,CURRENT_VERSION主要用於檢查該檔案為合法的tinker patch 檔案。
然後寫入patchedDexSize,第四位寫入的是資料區的offset,可以看到先使用0站位,等所有的map list相關的offset書寫結束,寫入當前的位置。

接下來寫入所有的跟maplist各個區域相關的offset(這裡各個區域的排序不重要,讀寫一致即可)
然後執行每個演算法寫入對應區域的資訊,最後生成patch檔案

其實就是對每個區域比較後將比較的結果寫入patch檔案中,檔案格式寫在DexDataBuffer中
生成的檔案以dex結尾,但需要注意的是,它不是真正的dex檔案,具體格式分析在DexDataBuffer中。

其中writePatchOperations方法就是寫入的方法,我們還是隻看stringDataSectionDiffAlg的:

private <T extends Comparable<T>> void writePatchOperations(
        DexDataBuffer buffer, List<PatchOperation<T>> patchOperationList
) {
    List<Integer> delOpIndexList = new ArrayList<>(patchOperationList.size());
    List<Integer> addOpIndexList = new ArrayList<>(patchOperationList.size());
    List<Integer> replaceOpIndexList = new ArrayList<>(patchOperationList.size());
    List<T> newItemList = new ArrayList<>(patchOperationList.size());

    for (PatchOperation<T> patchOperation : patchOperationList) {
        switch (patchOperation.op) {
            case PatchOperation.OP_DEL: {
                delOpIndexList.add(patchOperation.index);
                break;
            }
            case PatchOperation.OP_ADD: {
                addOpIndexList.add(patchOperation.index);
                newItemList.add(patchOperation.newItem);
                break;
            }
            case PatchOperation.OP_REPLACE: {
                replaceOpIndexList.add(patchOperation.index);
                newItemList.add(patchOperation.newItem);
                break;
            }
        }
    }

    buffer.writeUleb128(delOpIndexList.size());
    int lastIndex = 0;
    for (Integer index : delOpIndexList) {
        buffer.writeSleb128(index - lastIndex);
        lastIndex = index;
    }

    buffer.writeUleb128(addOpIndexList.size());
    lastIndex = 0;
    for (Integer index : addOpIndexList) {
        buffer.writeSleb128(index - lastIndex);
        lastIndex = index;
    }

    buffer.writeUleb128(replaceOpIndexList.size());
    lastIndex = 0;
    for (Integer index : replaceOpIndexList) {
        buffer.writeSleb128(index - lastIndex);
        lastIndex = index;
    }

    for (T newItem : newItemList) {
        if (newItem instanceof StringData) {
            buffer.writeStringData((StringData) newItem);
        } else
        /***********其他*******************/
    }
}

從程式碼中我們可以看出我們的寫入步驟:首先把patchOperationList轉化為3個OpIndexList,分別對應DEL,ADD,REPLACE,以及將所有的item存入newItemList。
然後依次寫入:

  1. del操作的個數,每個del的index
  2. add操作的個數,每個add的index
  3. replace操作的個數,每個需要replace的index
  4. 依次寫入newItemList.

最好來看看我們生成的patch是什麼樣子的:

  1. 首先包含幾個欄位,證明自己是tinker patch
  2. 包含生成newDex各個區域的offset,即可以將newDex劃分了多個區域,定位到起點
  3. 包含newDex各個區域的Item的刪除的索引(oldDex),新增的索引和值,替換的索引和值

那麼這麼看,我們猜測Patch的邏輯時這樣的:

  1. 首先根據各個區域的offset,確定各個區域的起點
  2. 讀取oldDex各個區域的items,然後根據patch中去除掉oldDex中需要刪除的和需要替換的item,再加上新增的item和替換的item即可組成newOld該區域的items。

所以,newDex的某個區域的包含:
oldItems - del - replace + addItems + replaceItems

這樣就完成了補丁包的生成過程,那麼伺服器在下發補丁之後如何合成全量的新Dex的呢?下面我們來分析:

二、補丁包下發後生成全量Dex;

如何合成全量的新Dex來執行

當app收到伺服器下發的補丁後,會觸發DefaultPatchListener.onPatchReceived事件,呼叫TinkerPatchService.runPatchService啟動patch程序進行補丁patch工作。

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

我們主要分析DexDiffPatchInternal.tryRecoverDexFiles,討論dex的patch過程。

tryRecoverDexFiles呼叫DexDiffPatchInternal.patchDexFile:

private static void patchDexFile(
    ZipFile baseApk, ZipFile patchPkg, ZipEntry oldDexEntry, ZipEntry patchFileEntry,
    ShareDexDiffPatchInfo patchInfo, File patchedDexFile) throws IOException {
/**********省略N行程式碼  最終都會呼叫這個方法************/
   new DexPatchApplier(oldDexStream, patchFileStream).executeAndSaveTo(patchedDexFile);
}

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

public void executeAndSaveTo(File file) throws IOException {
    OutputStream os = null;
    try {
        os = new BufferedOutputStream(new FileOutputStream(file));
        executeAndSaveTo(os);
    } finally {
        if (os != null) {
            try {
                os.close();
            } catch (Exception e) {
                // ignored.
            }
        }
    }
}

其實就是呼叫了DexPatchApplier.executeAndSaveTo(os):
方法程式碼比較長,原始碼中也是分了三部分註釋:

executeAndSaveTo(os) 三部分之第一部分

 public void executeAndSaveTo(OutputStream out) throws IOException {
    // Before executing, we should check if this patch can be applied to
    // old dex we passed in.
    byte[] oldDexSign = this.oldDex.computeSignature(false);
    if (oldDexSign == null) {
        throw new IOException("failed to compute old dex's signature.");
    }
    if (this.patchFile == null) {
        throw new IllegalArgumentException("patch file is null.");
    }
    byte[] oldDexSignInPatchFile = this.patchFile.getOldDexSignature();
    if (CompareUtils.uArrCompare(oldDexSign, oldDexSignInPatchFile) != 0) {
        throw new IOException(
                String.format(
                        "old dex signature mismatch! expected: %s, actual: %s",
                        Arrays.toString(oldDexSign),
                        Arrays.toString(oldDexSignInPatchFile)
                )
        );
    }

    // Firstly, set sections' offset after patched, sort according to their offset so that
    // the dex lib of aosp can calculate section size.
    TableOfContents patchedToc = this.patchedDex.getTableOfContents();

    patchedToc.header.off = 0;
    patchedToc.header.size = 1;
    patchedToc.mapList.size = 1;

    patchedToc.stringIds.off
            = this.patchFile.getPatchedStringIdSectionOffset();
    patchedToc.typeIds.off
            = this.patchFile.getPatchedTypeIdSectionOffset();
    patchedToc.typeLists.off
    /*****省略其他演算法過程************/

    Arrays.sort(patchedToc.sections);

    patchedToc.computeSizesFromOffsets();

// Firstly, set sections’ offset after patched, sort according to their offset so that
// the dex lib of aosp can calculate section size.
這裡實際上,就是讀取patchFile中記錄的值給patchedDex的TableOfContent中各種Section(大致對應map list中各個map_list_item)賦值,即設定各個區域的偏移量。

然後就是排序,設定byteCount等欄位資訊。patchedDex是最終合成的dex。

executeAndSaveTo(os) 三部分之第二部分

    // Secondly, run patch algorithms according to sections' dependencies.
    this.stringDataSectionPatchAlg = new StringDataSectionPatchAlgorithm(
            patchFile, oldDex, patchedDex, oldToPatchedIndexMap
    );
    this.typeIdSectionPatchAlg = new TypeIdSectionPatchAlgorithm(
            patchFile, oldDex, patchedDex, oldToPatchedIndexMap
    );
    /***省略其他演算法程式碼*****/

    this.stringDataSectionPatchAlg.execute();
    this.typeIdSectionPatchAlg.execute();

    /***省略其他演算法程式碼*****/

第二部分其實是將15種演算法初始化了一遍,然後都去執行execute()。我們依然是拿stringDataSectionPatchAlg來分析,其實還是呼叫的抽象父類DexSectionPatchAlgorithm中的execute方法:

public void execute() {
    final int deletedItemCount = patchFile.getBuffer().readUleb128();
    final int[] deletedIndices = readDeltaIndiciesOrOffsets(deletedItemCount);

    final int addedItemCount = patchFile.getBuffer().readUleb128();
    final int[] addedIndices = readDeltaIndiciesOrOffsets(addedItemCount);

    final int replacedItemCount = patchFile.getBuffer().readUleb128();
    final int[] replacedIndices = readDeltaIndiciesOrOffsets(replacedItemCount);

    final TableOfContents.Section tocSec = getTocSection(this.oldDex);
    Dex.Section oldSection = null;

    int oldItemCount = 0;
    if (tocSec.exists()) {
        oldSection = this.oldDex.openSection(tocSec);
        oldItemCount = tocSec.size;
    }

    // Now rest data are added and replaced items arranged in the order of
    // added indices and replaced indices.
    doFullPatch(
            oldSection, oldItemCount, deletedIndices, addedIndices, replacedIndices
    );
}

我們在寫入的時候現在都被讀取出來了,這裡的演算法和生成補丁的DexDiff是一個逆向的過程,每個區域的合併演算法採用二路歸併,在old dex的基礎上對元素進行刪除,增加,替換操作。:

  1. del操作的個數,每個del的index,儲存在一個int[] deletedIndices 中;
  2. add操作的個數,每個add的index,儲存在一個int[] addedIndices 中;
  3. replace操作的個數,每個需要replace的index,儲存在一個int[] replacedIndices 中;

接下來獲取了oldDex中oldItems和oldItemCount。然後帶著這些引數執行方法doFullPatch(oldSection, oldItemCount, deletedIndices, addedIndices, replacedIndices):

private void doFullPatch(
        Dex.Section oldSection,
        int oldItemCount,
        int[] deletedIndices,
        int[] addedIndices,
        int[] replacedIndices
) {
    int deletedItemCount = deletedIndices.length;
    int addedItemCount = addedIndices.length;
    int replacedItemCount = replacedIndices.length;
    int newItemCount = oldItemCount + addedItemCount - deletedItemCount;

    int deletedItemCounter = 0;
    int addActionCursor = 0;
    int replaceActionCursor = 0;

    int oldIndex = 0;
    int patchedIndex = 0;
    while (oldIndex < oldItemCount || patchedIndex < newItemCount) {
        if (addActionCursor < addedItemCount && addedIndices[addActionCursor] == patchedIndex) {
            /****************第1部分******************/
            T addedItem = nextItem(patchFile.getBuffer());
            int patchedOffset = writePatchedItem(addedItem);
            ++addActionCursor;
            ++patchedIndex;
        } else
        if (replaceActionCursor < replacedItemCount && replacedIndices[replaceActionCursor] == patchedIndex) {
            /****************第2部分 省略N行程式碼,和上一部分類似,後面會做具體分析******************/
            int patchedOffset = writePatchedItem(addedItem);
        } else
        if (Arrays.binarySearch(deletedIndices, oldIndex) >= 0) {
            /****************第3部分(1) 省略N行程式碼,和上一部分類似,後面會做具體分析******************/
            int patchedOffset = writePatchedItem(addedItem);
        } else
        if (Arrays.binarySearch(replacedIndices, oldIndex) >= 0) {
            /****************第3部分(2) 省略N行程式碼,和上一部分類似,後面會做具體分析******************/
            int patchedOffset = writePatchedItem(addedItem);
        } else
        if (oldIndex < oldItemCount) {
            /****************第4部分 省略N行程式碼,和上一部分類似,後面會做具體分析******************/
            int patchedOffset = writePatchedItem(addedItem);
        }
    }

    if (addActionCursor != addedItemCount || deletedItemCounter != deletedItemCount
            || replaceActionCursor != replacedItemCount
    ) {
        throw new IllegalStateException(
              /*************..String。。。。。。。。/
                )
        );
    }
}

到此,生成Dex過程完成。
從原始碼中可以看出我們是向位於patchedDex的stringData區寫資料,按照上面我們說的,應該要寫入新增的、替換的的資料,而我們寫入的過程:

首先計算出newItemCount=oldItemCount + addCount - delCount,然後開始遍歷,遍歷條件為0~oldItemCount或0~newItemCount。
而在patchIndex從0~newItemCount之間都會寫入對應的Item。

Item寫入通過程式碼我們可以看到(第1、2、3(1)、3(2)、4部分),具體程式碼如下:
1. 首先判斷該patchIndex是否包含在addIndices中,如果包含則寫入:

  if (addActionCursor < addedItemCount && addedIndices[addActionCursor] == patchedIndex) {
            T addedItem = nextItem(patchFile.getBuffer());
            int patchedOffset = writePatchedItem(addedItem);
            ++addActionCursor;
            ++patchedIndex;
  } 
  1. 再者判斷是否在repalceIndices中,如果包含則寫入:

    if (replaceActionCursor < replacedItemCount && replacedIndices[replaceActionCursor] == patchedIndex) {
    T replacedItem = nextItem(patchFile.getBuffer());
    int patchedOffset = writePatchedItem(replacedItem);
    ++replaceActionCursor;
    ++patchedIndex;
    }

  2. 然後判斷如果發現oldIndex被delete或者replace,直接跳過:

    if (Arrays.binarySearch(deletedIndices, oldIndex) >= 0) {
    T skippedOldItem = nextItem(oldSection); // skip old item.
    markDeletedIndexOrOffset(
    oldToPatchedIndexMap,
    oldIndex,
    getItemOffsetOrIndex(oldIndex, skippedOldItem)
    );
    ++oldIndex;
    ++deletedItemCounter;
    } else
    if (Arrays.binarySearch(replacedIndices, oldIndex) >= 0) {
    T skippedOldItem = nextItem(oldSection); // skip old item.
    markDeletedIndexOrOffset(
    oldToPatchedIndexMap,
    oldIndex,
    getItemOffsetOrIndex(oldIndex, skippedOldItem)
    );
    ++oldIndex;
    }

  3. 最後一個index指的就是,oldIndex為非delete和replace的,也就是和newDex中items相同的部分。

    if (oldIndex < oldItemCount) {
    T oldItem = adjustItem(this.oldToPatchedIndexMap, nextItem(oldSection));

     int patchedOffset = writePatchedItem(oldItem);
    
     updateIndexOrOffset(
             this.oldToPatchedIndexMap,
             oldIndex,
             getItemOffsetOrIndex(oldIndex, oldItem),
             patchedIndex,
             patchedOffset
     );
    
     ++oldIndex;
     ++patchedIndex;
    

    }

上述1.2.4三個部分即可組成完整的newDex的該區域。完成了stringData區域的patch演算法。
其他的14種演算法的execute程式碼是相同的(父抽象類),執行的操作類似,都會完成各個部分的patch演算法。
當所有的區域都完成恢復後,那麼剩下的就是header和mapList了,所以回到所有演算法執行完成的地方,即executeAndSaveTo(OutputStream out)的第三部分:

executeAndSaveTo(os) 三部分之第三部分

public void executeAndSaveTo(OutputStream out) throws IOException {

        /************省略this.stringDataSectionPatchAlg.execute()前的程式碼*********/
    this.stringDataSectionPatchAlg.execute();
    /******省略其他演算法執行execute()******************/

    // Thirdly, write header, mapList. Calculate and write patched dex's sign and checksum.
    Dex.Section headerOut = this.patchedDex.openSection(patchedToc.header.off);
    patchedToc.writeHeader(headerOut);

    Dex.Section mapListOut = this.patchedDex.openSection(patchedToc.mapList.off);
    patchedToc.writeMap(mapListOut);

    this.patchedDex.writeHashes();

    // Finally, write patched dex to file.
    this.patchedDex.writeTo(out);
}

可以看到首先是定位到header區域,寫header相關資料;定位到map list區域,編寫map list相關資料。兩者都完成的時候,需要編寫header中比較特殊的兩個欄位:簽名和checkSum,因為這兩個欄位是依賴map list的,所以必須在編寫map list後。
這樣就完成了完整的dex的生成,最後將記憶體中的所有資料寫到檔案中。

三、生成全量Dex後的載入過程

上述是完整Dex的生成過程,也是演算法的核心所在,所以花了很長時間,下面就是我們生成完整Dex後的載入過程咯,這一部分主要是在這個包下:

tinker-loader

TinkerApplication通過反射的方式將實際的app業務隔離,這樣可以在熱更新的時候修改實際的app內容。

在TinkerApplication中的onBaseContextAttached中會通過反射呼叫TinkerLoader的tryLoad載入已經合成的dex。

 private static final String TINKER_LOADER_METHOD   = "tryLoad";
 private void loadTinker() {
    //disable tinker, not need to install
    if (tinkerFlags == TINKER_DISABLE) {
        return;
    }
    tinkerResultIntent = new Intent();
    try {
        //reflect tinker loader, because loaderClass may be define by user!
        Class<?> tinkerLoadClass = Class.forName(loaderClassName, false, getClassLoader());

        Method loadMethod = tinkerLoadClass.getMethod(TINKER_LOADER_METHOD, TinkerApplication.class);
        Constructor<?> constructor = tinkerLoadClass.getConstructor();
        tinkerResultIntent = (Intent) loadMethod.invoke(constructor.newInstance(), this);
    } catch (Throwable e) {
        //has exception, put exception error code
        ShareIntentUtil.setIntentReturnCode(tinkerResultIntent, ShareConstants.ERROR_LOAD_PATCH_UNKNOWN_EXCEPTION);
        tinkerResultIntent.putExtra(INTENT_PATCH_EXCEPTION, e);
    }
}

下面是反射呼叫的TinkerLoader中的tryLoad方法:

@Override
public Intent tryLoad(TinkerApplication app) {
    Intent resultIntent = new Intent();

    long begin = SystemClock.elapsedRealtime();
    tryLoadPatchFilesInternal(app, resultIntent);
    long cost = SystemClock.elapsedRealtime() - begin;
    ShareIntentUtil.setIntentPatchCostTime(resultIntent, cost);
    return resultIntent;
}

其中tryLoadPatchFilesInternal是載入Patch檔案的核心函式(程式碼比較多,大家看註釋應該就可以明白每段是做什麼的了):

private void tryLoadPatchFilesInternal(TinkerApplication app, Intent resultIntent) {
    final int tinkerFlag = app.getTinkerFlags();

    if (!ShareTinkerInternals.isTinkerEnabled(tinkerFlag)) {
        //tinkerFlag是否開啟,否則不載入
        Log.w(TAG, "tryLoadPatchFiles: tinker is disable, just return");
        ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_DISABLE);
        return;
    }
    //tinker
    File patchDirectoryFile = SharePatchFileUtil.getPatchDirectory(app);
    if (patchDirectoryFile == null) {
        //tinker目錄是否生成,沒有則表示沒有生成全量的dex,不需要重新載入
        Log.w(TAG, "tryLoadPatchFiles:getPatchDirectory == null");
        //treat as not exist
        ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_DIRECTORY_NOT_EXIST);
        return;
    }
    //tinker/patch.info
    File patchInfoFile = SharePatchFileUtil.getPatchInfoFile(patchDirectoryPath);

    //check patch info file whether exist
    if (!patchInfoFile.exists()) {
        //tinker/patch.info是否存在,否則不載入
        Log.w(TAG, "tryLoadPatchFiles:patch info not exist:" + patchInfoFile.getAbsolutePath());
        ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_INFO_NOT_EXIST);
        return;
    }
    //old = 641e634c5b8f1649c75caf73794acbdf
    //new = 2c150d8560334966952678930ba67fa8
    File patchInfoLockFile = SharePatchFileUtil.getPatchInfoLockFile(patchDirectoryPath);

    patchInfo = SharePatchInfo.readAndCheckPropertyWithLock(patchInfoFile, patchInfoLockFile);
    if (patchInfo == null) {
        //讀取patch.info,讀取失敗則不載入
        ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_INFO_CORRUPTED);
        return;
    }

    String oldVersion = patchInfo.oldVersion;
    String newVersion = patchInfo.newVersion;
    String oatDex = patchInfo.oatDir;

    if (oldVersion == null || newVersion == null || oatDex == null) {
        //判斷版本號是否為空,為空則不載入
        //it is nice to clean patch
        Log.w(TAG, "tryLoadPatchFiles:onPatchInfoCorrupted");
        ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_INFO_CORRUPTED);
        return;
    }

    resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_OLD_VERSION, oldVersion);
    resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_NEW_VERSION, newVersion);

    //tinker/patch.info/patch-641e634c
    String patchVersionDirectory = patchDirectoryPath + "/" + patchName;

    File patchVersionDirectoryFile = new File(patchVersionDirectory);

    if (!patchVersionDirectoryFile.exists()) {
        //判斷patch version directory(//tinker/patch.info/patch-641e634c)是否存在
        ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_VERSION_DIRECTORY_NOT_EXIST);
        return;
    }

    //tinker/patch.info/patch-641e634c/patch-641e634c.apk
    File patchVersionFile = new File(patchVersionDirectoryFile.getAbsolutePath(), SharePatchFileUtil.getPatchVersionFile(version));

    if (!SharePatchFileUtil.isLegalFile(patchVersionFile)) {
        //判斷patchVersionDirectoryFile(//tinker/patch.info/patch-641e634c/patch-641e634c.apk)是否存在
        Log.w(TAG, "tryLoadPatchFiles:onPatchVersionFileNotFound");
        //we may delete patch info file
        ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_VERSION_FILE_NOT_EXIST);
        return;
    }

    ShareSecurityCheck securityCheck = new ShareSecurityCheck(app);

    int returnCode = ShareTinkerInternals.checkTinkerPackage(app, tinkerFlag, patchVersionFile, securityCheck);
    if (returnCode != ShareConstants.ERROR_PACKAGE_CHECK_OK) {
        //checkTinkerPackage,(如tinkerId和oldTinkerId不能相等,否則不載入)
        Log.w(TAG, "tryLoadPatchFiles:checkTinkerPackage");
        resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_PACKAGE_PATCH_CHECK, returnCode);
        ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_PACKAGE_CHECK_FAIL);
        return;
    }

    if (isEnabledForDex) {
        //tinker/patch.info/patch-641e634c/dex
        boolean dexCheck = TinkerDexLoader.checkComplete(patchVersionDirectory, securityCheck, oatDex, resultIntent);
        if (!dexCheck) {
            //檢測dex的完整性,包括dex是否全部生產,是否對dex做了優化,優化後的檔案是否存在(//tinker/patch.info/patch-641e634c/dex)
            //file not found, do not load patch
            Log.w(TAG, "tryLoadPatchFiles:dex check fail");
            return;
        }
    }
    /****省略對so res檔案進行完整性檢測***************/
    final boolean isEnabledForNativeLib = ShareTinkerInternals.isTinkerEnabledForNativeLib(tinkerFlag);
    /***************************************/
    //now we can load patch jar
    if (isEnabledForDex) {
        /********************劃重點---TinkerDexLoader.loadTinkerJars********************/
        boolean loadTinkerJars = TinkerDexLoader.loadTinkerJars(app, patchVersionDirectory, oatDex, resultIntent, isSystemOTA);

        if (isSystemOTA) {
            // update fingerprint after load success
            patchInfo.fingerPrint = Build.FINGERPRINT;
            patchInfo.oatDir = loadTinkerJars ? ShareConstants.INTERPRET_DEX_OPTIMIZE_PATH : ShareConstants.DEFAULT_DEX_OPTIMIZE_PATH;
            // reset to false
            oatModeChanged = false;

            if (!SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, patchInfo, patchInfoLockFile)) {
                ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_REWRITE_PATCH_INFO_FAIL);
                Log.w(TAG, "tryLoadPatchFiles:onReWritePatchInfoCorrupted");
                return;
            }
            // update oat dir
            resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_OAT_DIR, patchInfo.oatDir);
        }
        if (!loadTinkerJars) {
            Log.w(TAG, "tryLoadPatchFiles:onPatchLoadDexesFail");
            return;
        }
    }

    return;
}

其中TinkerDexLoader.loadTinkerJars是用來處理載入dex檔案。

public static boolean loadTinkerJars(final TinkerApplication application, String directory, String oatDir, Intent intentResult, boolean isSystemOTA) {

 /*****省略部分程式碼****************/
 PathClassLoader classLoader = (PathClassLoader) TinkerDexLoader.class.getClassLoader();
 /***********省略N行程式碼,主要是生成一些合法檔案列表,對dex檔案進行優化**************/
 // 載入Dex
 SystemClassLoaderAdder.installDexes(application, classLoader, optimizeDir, legalFiles);

}

然後 SystemClassLoaderAdder.installDexes 根據安卓的版本對dex進行安裝啦:

@SuppressLint("NewApi")
public static void installDexes(Application application, PathClassLoader loader, File dexOptDir, List<File> files)
    throws Throwable {
    Log.i(TAG, "installDexes dexOptDir: " + dexOptDir.getAbsolutePath() + ", dex size:" + files.size());

    if (!files.isEmpty()) {
        files = createSortedAdditionalPathEntries(files);
        ClassLoader classLoader = loader;
        if (Build.VERSION.SDK_INT >= 24 && !checkIsProtectedApp(files)) {
            classLoader = AndroidNClassLoader.inject(loader, application);
        }
        //because in dalvik, if inner class is not the same classloader with it wrapper class.
        //it won't fail at dex2opt
        if (Build.VERSION.SDK_INT >= 23) {
            V23.install(classLoader, files, dexOptDir);
        } else if (Build.VERSION.SDK_INT >= 19) {
            V19.install(classLoader, files, dexOptDir);
        } else if (Build.VERSION.SDK_INT >= 14) {
            V14.install(classLoader, files, dexOptDir);
        } else {
            V4.install(classLoader, files, dexOptDir);
        }
        //install done
        sPatchDexCount = files.size();
        Log.i(TAG, "after loaded classloader: " + classLoader + ", dex size:" + sPatchDexCount);

        if (!checkDexInstall(classLoader)) {
            //reset patch dex
            SystemClassLoaderAdder.uninstallPatchDex(classLoader);
            throw new TinkerRuntimeException(ShareConstants.CHECK_DEX_INSTALL_FAIL);
        }
    }
}

前面我們講載入類一般使用的是PathClassLoader和DexClassLoader,而PathClassLoader作為系統類和應用類的載入器。DexClassLoader 用來從.jar和.apk型別的檔案內部載入classes.dex檔案。

而install是怎麼做的呢:

/**
 *Installer for platform versions 23.
 */
private static final class V23 {

    private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
                                File optimizedDirectory)
        throws IllegalArgumentException, IllegalAccessException,
        NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
        /* The patched class loader is expected to be a descendant of
         *dalvik.system.BaseDexClassLoader. We modify its
         *dalvik.system.DexPathList pathList field to append additional DEX
         *file entries.
         */
        Field pathListField = ShareReflectUtil.findField(loader, "pathList");
        Object dexPathList = pathListField.get(loader);
        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
        ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", makePathElements(dexPathList,
            new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
            suppressedExceptions));
        if (suppressedExceptions.size() > 0) {
            for (IOException e : suppressedExceptions) {
                Log.w(TAG, "Exception in makePathElement", e);
                throw e;
            }

        }
    }
    /*************省略makePathElements方法***************/
}

先獲取BaseDexClassLoader的dexPathList物件,然後通過dexPathList的makeDexElements方法將我們要安裝的dex轉化成Element[]物件,最後將其和dexPathList的dexElements物件進行合併,就是新的Element[]物件,因為我們新增的dex都被放在dexElements陣列的最前面,所以當通過findClass來查詢這個類時,就是使用的我們最新的dex裡面的類。不同版本里面的DexPathList等類的函式和欄位都有一些變化,其他類似。

到此為止,dex的整個載入過程就結束了!

其他使用Tinker進行更新的,如so庫的更新、library的更新大家可以在原始碼中按照上面的dex載入過程看到。

熱更新方案的對比

好了,上面我們也說了幾種熱更新的方案了,其他的熱更新方案大家可以去搜索瞭解。

上面阿里給出了AndFix和HotFix以及Sophix的對比,現在我們就對時下的幾種熱更新方案進行對比,看看到底哪種好:

方案對比1

從對比中我們也能發現Sophix和Tinker作為兩大巨頭的最新熱更新方案,都是比較厲害的,大家如果有需要的話可以去嘗試下。

因為時間關係,實現自己的熱更新方案還沒有寫完,暫時不放出來了,等我寫完了會放上鍊接的。