Tinker的patch生成過程

Tinker的patch生成方式有兩種棉姐,一種是通過命令行,一種是通過gradle的方式掰茶。下面我們簡單介紹下gradle的方式阎毅。這篇文章包括一下幾部分

patch文件的組成
生成流程
patch文件的生成
dex的差分
so的差分
生成meta,版本文件等

涉及目錄

Patch打包的涉及的目錄是tinker-build屈梁。其中命令行涉及的目錄是tinker-patch-cli嗤练, gradle涉及的目錄是tinker-patch-gradle-plugin。命令行和gradle只是入口不一樣在讶,而patch生成的代碼主要在tinker-patch-lib目錄中煞抬。tinker中dex的差分是微信自己實(shí)現(xiàn)的一套dexdiff算法,差分的算法代碼主要在tinker-commons中构哺,其中DexPatchApplier是進(jìn)行生成patch的入口革答。那么其他文件的差分呢,比如so,resource文件的差分是采用的什么算法呢遮婶?

patch文件的組成

changed_classes.dex
META_INF
----XXX.RSA
----XXX.SF
----XXX.MF
test.dex
assets
----package_meta.txt
----dex_meta.txt
YAPATCH.MF

其中changed_classes.dex和test.dex是dex蝗碎。test.dex是為了驗(yàn)證dex的加載是否成功湖笨,test.dex中含有com.tencent.tinker.loader.TinkerTestDexLoad類旗扑,該類中包含一個(gè)字段isPatch,補(bǔ)丁的加載校驗(yàn)是在SystemClassLoaderAdder中的checkDexInstall方法慈省。
checkDexInstall就是通過findField該字段判斷是否加載成功臀防。

需要注意的是這個(gè)test.dex是從目錄下直接拷貝的,而不是直接生成的边败,test.dex中只有一個(gè)TinkerTestDexLoad類袱衷,而且其中的屬性isPatch是true。因此如果沒有加載patch,就會直接加載打包進(jìn)apk中的TinkerTestDexLoad笑窜,此時(shí)的isPatch屬性為false.如果加載補(bǔ)丁成功致燥,就會從patch中的class.dex中讀取TinkerTestDexLoad這個(gè)類,而class.dex中TinkerTestDexLoad的屬性isPatch是true排截。因此可以使用讀取patch的值來作為補(bǔ)丁是否加載成功的依據(jù)嫌蚤。

而changed_classes.dex可能因?yàn)楦膭哟a的范圍而生成多個(gè)changed_class。 根據(jù)不同的情況,最多有四個(gè)文件是以meta.txt結(jié)尾的:

package_meta.txt 補(bǔ)丁包的基本信息
dex_meta.txt dex補(bǔ)丁的信息
so_meta.txt so補(bǔ)丁的信息
res_meta.txt 資源補(bǔ)丁的信息

package_meta.txt中的格式范例如下:

#base package config field
#Tue Jun 25 15:32:59 CST 2019
NEW_TINKER_ID=XXXXXX-patch
TINKER_ID=XXXXXX-base

而dex_meta.txt中的格式范例如下:

changed_classes.dex,,5d4ce4b80d4d5168006a63a5a16d94b3,5d4ce4b80d4d5168006a63a5a16d94b3,0,0,0,jar
test.dex,,56900442eb5b7e1de45449d0685e6e00,56900442eb5b7e1de45449d0685e6e00,0,0,0,jar

而res_meta.txt文件的格式范例如下:

resources_out.zip,4019114434,6148149bd5ed4e0c2f5357c6e2c577d6
pattern:4
resources.arsc
r/*
res/*
assets/*
modify:1
r/g/ag.xml
add:1
assets/only_use_to_test_tinker_resource.txt

生成流程

下面分析比較常用的gradle方法生成patch的流程断傲。gradle插件的入口是TinkerPatchPlugin脱吱。其中調(diào)用了多個(gè)task,名字及類型如下所示:

tinkerPatch${variantName}                 類型:TinkerPatchSchemaTask
tinkerProcess${variantName}Manifest       類型:TinkerManifestTask
tinkerProcess${variantName}ResourceId     類型:TinkerResourceIdTask
tinkerProcess${variantName}Proguard       類型:TinkerProguardConfigTask
tinkerProcess${variantName}MultidexKeep   類型:TinkerMultidexConfigTask

我們先重點(diǎn)看下tinkerPatch${variantName}這個(gè)task认罩,這個(gè)task是TinkerPatchSchemaTask類型箱蝠,在TinkerPatchPlugin的setPatchNewApkPath方法中有如下一句代碼:

tinkerPatchBuildTask.dependsOn variant.assemble

因此在對應(yīng)的variant執(zhí)行完assemble之后,就會執(zhí)行tinkerPatch${variantName}。

// com.tencent.tinker.build.gradle.task.TinkerPatchSchemaTask
@TaskAction
def tinkerPatch() {
    //開始打包patch
    configuration.checkParameter()
    configuration.buildConfig.checkParameter()
    configuration.res.checkParameter()
    configuration.dex.checkDexMode()
    configuration.sevenZip.resolveZipFinalPath()

    InputParam.Builder builder = new InputParam.Builder()
    if (configuration.useSign) {
        if (signConfig == null) {
            throw new GradleException("can't the get signConfig for this build")
        }
        builder.setSignFile(signConfig.storeFile)
                .setKeypass(signConfig.keyPassword)
                .setStorealias(signConfig.keyAlias)
                .setStorepass(signConfig.storePassword)
    }

    // patch的參數(shù)宦搬,從tinker.gradle中讀取配置信息
    builder.setOldApk(configuration.oldApk)
            .setNewApk(buildApkPath)
            .setOutBuilder(outputFolder)
            .setIgnoreWarning(configuration.ignoreWarning)
            .setAllowLoaderInAnyDex(configuration.allowLoaderInAnyDex)
            .setRemoveLoaderForAllDex(configuration.removeLoaderForAllDex)
            .setDexFilePattern(new ArrayList<String>(configuration.dex.pattern))
            .setIsProtectedApp(configuration.buildConfig.isProtectedApp)//  note  isProtectedApp,是在tinker.gradle中配置
            .setIsComponentHotplugSupported(configuration.buildConfig.supportHotplugComponent)
            .setDexLoaderPattern(new ArrayList<String>(configuration.dex.loader))
            .setDexIgnoreWarningLoaderPattern(new ArrayList<String>(configuration.dex.ignoreWarningLoader))
            .setDexMode(configuration.dex.dexMode)
            .setSoFilePattern(new ArrayList<String>(configuration.lib.pattern))
            .setResourceFilePattern(new ArrayList<String>(configuration.res.pattern))
            .setResourceIgnoreChangePattern(new ArrayList<String>(configuration.res.ignoreChange))
            .setResourceIgnoreChangeWarningPattern(new ArrayList<String>(configuration.res.ignoreChangeWarning))
            .setResourceLargeModSize(configuration.res.largeModSize)
            .setUseApplyResource(configuration.buildConfig.usingResourceMapping)
            .setConfigFields(new HashMap<String, String>(configuration.packageConfig.getFields()))
            .setSevenZipPath(configuration.sevenZip.path)
            .setUseSign(configuration.useSign)
            .setArkHotPath(configuration.arkHot.path)
            .setArkHotName(configuration.arkHot.name)

    InputParam inputParam = builder.create()
    Runner.gradleRun(inputParam);
}

這個(gè)方法首先從tinker.gradle中讀取相關(guān)配置牙瓢,然后作為參數(shù),開始調(diào)用Runner.gradleRun方法開始準(zhǔn)備生成patch文件间校。gradleRun中調(diào)用來run方法一罩,run中直接調(diào)用來tinkerPatch, 這個(gè)方法就真正開始創(chuàng)建patch撇簿。下面我們看下這個(gè)方法聂渊,代碼如下:

// com.tencent.tinker.build.patch.Runner  
protected void tinkerPatch() {
    Logger.d("-----------------------Tinker patch begin-----------------------");

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

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

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

    } catch (Throwable e) {
        goToError(e, ERRNO_USAGE);
    }

    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", mConfig.mOutFolder);
    Logger.d("-----------------------Tinker patch end-------------------------");
}

前面講過patch文件的組成,在tinkerPatch也是分幾步生成四瘫,首先生成dex等patch文件汉嗽,然后生成meta和版本等文件,最后將前兩步生成的文件打包成patch找蜜。

patch文件的生成

下面我們先重點(diǎn)看下patch的第一部分的生成饼暑。 ApkDecoder的構(gòu)造方法如下:

// com.tencent.tinker.build.decoder.ApkDecoder
public ApkDecoder(Configuration config) throws IOException {
        super(config);
        this.mNewApkDir = config.mTempUnzipNewDir;
        this.mOldApkDir = config.mTempUnzipOldDir;

        this.manifestDecoder = new ManifestDecoder(config);

        //put meta files in assets
        String prePath = TypedValue.FILE_ASSETS + File.separator;
        dexPatchDecoder = new UniqueDexDiffDecoder(config, prePath + TypedValue.DEX_META_FILE, TypedValue.DEX_LOG_FILE);
        soPatchDecoder = new BsDiffDecoder(config, prePath + TypedValue.SO_META_FILE, TypedValue.SO_LOG_FILE);
        resPatchDecoder = new ResDiffDecoder(config, prePath + TypedValue.RES_META_TXT, TypedValue.RES_LOG_FILE);
        arkHotDecoder = new ArkHotDecoder(config, prePath + TypedValue.ARKHOT_META_TXT);
        Logger.d("config: " + config.mArkHotPatchPath + " " + config.mArkHotPatchName + prePath + TypedValue.ARKHOT_META_TXT);
        resDuplicateFiles = new ArrayList<>();
    }

ApkDecoder有ManifestDecoder,UniqueDexDiffDecoder洗做,BsDiffDecoder弓叛,ResDiffDecoder,ArkHotDecoder類型的多個(gè)Decoder诚纸,在構(gòu)造方法將這幾個(gè)成員變量初始化撰筷。 各個(gè)Decoder分別針對代碼中不同的部分進(jìn)行patch。都是繼承自BaseDecoder畦徘,然后實(shí)現(xiàn)了patch毕籽,onAllPatchesStart,onAllPatchesEnd這個(gè)三個(gè)抽象方法井辆。 創(chuàng)建了ApkDecoder實(shí)例decoder后关筒,調(diào)用onAllPatchesStart進(jìn)行patch之前的準(zhǔn)備工作。onAllPatchesStart代碼如下:

@Override
public void onAllPatchesStart() throws IOException, TinkerPatchException {
    manifestDecoder.onAllPatchesStart();
    dexPatchDecoder.onAllPatchesStart();
    soPatchDecoder.onAllPatchesStart();
    resPatchDecoder.onAllPatchesStart();
}

隨后每個(gè)decoder分別執(zhí)行自己的onAllPatchesStart方法杯缺。調(diào)用onAllPatchesStart方法后蒸播,就調(diào)用ApkDecoder的patch方法開始生成patch。patch方法的代碼如下:

// com.tencent.tinker.build.decoder.ApkDecoder
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();
    arkHotDecoder.onAllPatchesEnd();

    //clean resources
    dexPatchDecoder.clean();
    soPatchDecoder.clean();
    resPatchDecoder.clean();
    arkHotDecoder.clean();

    return true;
}

按照代碼中的注釋萍肆,先檢查manifest文件的改動袍榆。然后調(diào)用Files.walkFileTree來生成patch。我們先看下manifestDecoder的patch的文件是如何檢查manifest文件的匾鸥,代碼入下:

// com.tencent.tinker.build.decoder.ManifestDecoder
public boolean patch(File oldFile, File newFile) throws IOException, TinkerPatchException {
    try {
        AndroidParser oldAndroidManifest = AndroidParser.getAndroidManifest(oldFile);
        AndroidParser newAndroidManifest = AndroidParser.getAndroidManifest(newFile);

        // Android版本低于14直接返回蜡塌,此處忽略
        final String oldXml = oldAndroidManifest.xml.trim();
        final String newXml = newAndroidManifest.xml.trim();
        final boolean isManifestChanged = !oldXml.equals(newXml);

        f (!isManifestChanged) {
            Logger.d("\nManifest has no changes, skip rest decode works.");
            return false;
        }
        ...... 省略對manifest變化的處理。
    }
}

這部分的邏輯是先調(diào)用AndroidParser的getAndroidManifest方法從文件中讀取到manifest的內(nèi)容勿负,然后拿出其中的xml進(jìn)行比較馏艾。如果沒有變化劳曹,直接返回。一般情況下manifest的變化都是新增四大組件導(dǎo)致的琅摩, 而熱更比較少新增铁孵,因此manifest變化的情況先跳過。另外manifest的修改了房资,后續(xù)進(jìn)行patch的合成加載進(jìn)行相應(yīng)的處理蜕劝。 回到ApkDecoder的patch方法繼續(xù)看,將要打包patch的兩個(gè)apk解壓后轰异。然后調(diào)用Files的walkFileTree方法來遍歷新的apk解壓后的目錄岖沛。這個(gè)Files是NIO中的類,walkFileTree方法傳遞兩個(gè)參數(shù)搭独,一個(gè)是 要遍歷的目錄婴削,第二個(gè)參數(shù)是遍歷行為控制器FileVisitor,它是一個(gè)接口牙肝,里面定義了4個(gè)方法用來指定當(dāng)你訪問一個(gè)節(jié)點(diǎn)之前唉俗、之中、之后配椭、失敗時(shí)應(yīng)該采取什么行動虫溜。這里看下ApkFilesVisitor這個(gè)類,實(shí)現(xiàn)了visitFile方法股缸,制定訪問文件時(shí)的操作行為衡楞。的構(gòu)造方法代碼如下:

// com.tencent.tinker.build.decoder.ApkDecoder
ApkFilesVisitor(Configuration config, Path newPath, Path oldPath, BaseDecoder dex, BaseDecoder so, BaseDecoder resDecoder) {
    this.config = config;
    this.dexDecoder = dex;
    this.soDecoder = so;
    this.resDecoder = resDecoder;
    this.newApkPath = newPath;
    this.oldApkPath = oldPath;
}

將在ApkDecoder構(gòu)造方法中初始化的各種decoder傳遞進(jìn)來,在遍歷目錄針對不同類型的文件調(diào)用不同的decoder乓序。下面看下遍歷文件visitFile這個(gè)方法寺酪,代碼如下:

com.tencent.tinker.build.decoder.ApkDecoder 
@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)) {//mDexFilePattern   "classes*.dex""classes*.dex"
        //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) {
            throw new RuntimeException(e);
        }
        return FileVisitResult.CONTINUE;
    }
    if (Utils.checkFileInPattern(config.mSoFilePattern, patternKey)) {//mSoFilePattern "lib/*/*.so"
        //also treat duplicate file as unchanged
        if (Utils.checkFileInPattern(config.mResFilePattern, patternKey) && oldFile != null) {
            resDuplicateFiles.add(oldFile);
        }
        try {
            soDecoder.patch(oldFile, file.toFile());
        } catch (Exception e) {
             throw new RuntimeException(e);
        }
        return FileVisitResult.CONTINUE;
    }
    if (Utils.checkFileInPattern(config.mResFilePattern, patternKey)) {// mResFilePattern  “res/*", "r/*", "assets/*", "resources.arsc"
        try {
            resDecoder.patch(oldFile, file.toFile());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return FileVisitResult.CONTINUE;
    }
    return FileVisitResult.CONTINUE;
}

針對符合config.mDexFilePattern命名規(guī)則的文件調(diào)用UniqueDexDiffDecoder進(jìn)行patch坎背,對符合config.mSoFilePattern命名規(guī)則的文件調(diào)用BsDiffDecoder進(jìn)行patch替劈,對符合config.mResFilePattern命名要求的文件調(diào)用ResDiffDecoder進(jìn)行patch合成。mDexFilePattern得滤,mSoFilePattern陨献,mResFilePattern這結(jié)果值是在tinker.gradle中進(jìn)行的配置,一般使用默認(rèn)就行懂更。下面先對UniqueDexDiffDecoder的patch進(jìn)行分析眨业,從名字上來看,我們離差分操作不遠(yuǎn)了沮协。

dex的差分

dex文件的查分操作在UniqueDexDiffDecoder的patch方法中龄捡,UniqueDexDiffDecoder繼承自DexDiffDecoder。UniqueDexDiffDecoder的patch操作主要是調(diào)用父類的patch操作慷暂,之后對文件名進(jìn)行重名判斷聘殖。我們主要看下DexDiffDecoder的patch方法,代碼如下:

// com.tencent.tinker.build.decoder.DexDiffDecoder
@Override
public boolean patch(final File oldFile, final File newFile) throws IOException, TinkerPatchException {
    final String dexName = getRelativeDexName(oldFile, newFile);
    // first of all, we should check input files if excluded classes were modified.
    Logger.d("Check for loader classes in dex: %s", dexName);

    try {
        excludedClassModifiedChecker.checkIfExcludedClassWasModifiedInNewDex(oldFile, newFile);
    } catch (IOException e) {
        throw new TinkerPatchException(e);
    } catch (TinkerPatchException e) {
        // 省略異常處理
    }

    // 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;
    }

    final String oldMd5 = getRawOrWrappedDexMD5(oldFile);
    if ((oldMd5 != null && !oldMd5.equals(newMd5)) || (oldMd5 == null && newMd5 != null)) {
        hasDexChanged = true;
        if (oldMd5 != null) {
            collectAddedOrDeletedClasses(oldFile, newFile);
        }
    }

    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;
}

patch這個(gè)方法主要是做一些準(zhǔn)備工作,準(zhǔn)備工作很繁瑣奸腺,需要處理的情況真多餐禁。而進(jìn)行差分工作主要在DexDiffDecoder的onAllPatchesEnd方法中,代碼如下

// com.tencent.tinker.build.decoder.DexDiffDecoder
@Override
public void onAllPatchesEnd() throws Exception {
    if (!hasDexChanged) {
        Logger.d("No dexes were changed, nothing needs to be done next.");
        return;
    }
    // Whether tinker should treat the base apk as the one being protected by app
    // protection tools.
    // If this attribute is true, the generated patch package will contain a
    // dex including all changed classes instead of any dexdiff patch-info files.
    if (config.mIsProtectedApp) {
        generateChangedClassesDexFile();
    } else {
        generatePatchInfoFile();
    }
    addTestDex();
}

可以看到針對是否是加固的APP突照,有不同的處理帮非。如果是加固的APP,則產(chǎn)生的package會包含所有的改動文件讹蘑。而對于非加固的文件末盔,只需要進(jìn)行dexdiff算法后生成的patch-info文件。這部分的說明來自TinkerBuildConfigExtension中對于的isProtectedApp的注釋座慰。generateChangedClassesDexFile這個(gè)方法使用的是dexlib2庫中的DexBuilder來生成dex文件庄岖。而generatePatchInfoFile這個(gè)方法使用的是DexPatchApplier,這個(gè)類是使用微信自研的dexDiff算法角骤。對于這個(gè)算法隅忿,簡單的介紹和示范在Android 熱修復(fù) Tinker 源碼分析之DexDiff / DexPatch,全面詳細(xì)的介紹在DexDiff。這個(gè)地方其實(shí)有點(diǎn)意外邦尊,因?yàn)橐恢币詾閐ex的patch是使用微信自研的dexDiff算法背桐,沒想到加固的APP并不是這套算法,而是dexlib2這個(gè)框架蝉揍。dex的patch這部分看了一天链峭,看得很暈,感謝厲害的大佬們又沾,以及更厲害的微信弊仪。

so的差分

so的patch是在BsDiffDecoder的patch中進(jìn)行,

// com.tencent.tinker.build.decoder.BsDiffDecoder
@Override
    public boolean patch(File oldFile, File newFile) throws IOException, TinkerPatchException {
        //first of all, we should check input files
        if (newFile == null || !newFile.exists()) {
            return false;
        }
        //new add file
        String newMd5 = MD5.getMD5(newFile);
        File bsDiffFile = getOutputPath(newFile).toFile();

        if (oldFile == null || !oldFile.exists()) {
            FileOperation.copyFileUsingStream(newFile, bsDiffFile);
            writeLogFiles(newFile, null, null, newMd5);
            return true;
        }

        //both file length is 0
        if (oldFile.length() == 0 && newFile.length() == 0) {
            return false;
        }
        if (oldFile.length() == 0 || newFile.length() == 0) {
            FileOperation.copyFileUsingStream(newFile, bsDiffFile);
            writeLogFiles(newFile, null, null, newMd5);
            return true;
        }

        //new add file
        String oldMd5 = MD5.getMD5(oldFile);
        if (oldMd5.equals(newMd5)) {
            return false;
        }
        if (!bsDiffFile.getParentFile().exists()) {
            bsDiffFile.getParentFile().mkdirs();
        }
        BSDiff.bsdiff(oldFile, newFile, bsDiffFile);
        if (Utils.checkBsDiffFileSize(bsDiffFile, newFile)) {
            writeLogFiles(newFile, oldFile, bsDiffFile, newMd5);
        } else {
            FileOperation.copyFileUsingStream(newFile, bsDiffFile);
            writeLogFiles(newFile, null, null, newMd5);
        }
        return true;
    }

如果是新增的資源文件杖刷,則直接拷貝励饵。否則使用BSDiff進(jìn)行diff操作,這是二進(jìn)制的差量算法滑燃,具體的介紹在這里BSDiff算法. 如果BSDiff.bsdiff方法生成的文件過大役听,就會直接當(dāng)做新增加的文件來對待,以避免patch時(shí)間過長表窘。

resource的差分

資源文件的diff在ResDiffDecoder這個(gè)類的patch方法中完成典予,采用的方法也是BSDiff算法。牽扯到知識真多乐严,真是功夫在詩外瘤袖。和so一樣,如果新增昂验,則直接拷貝捂敌。否則調(diào)用BSDiff算法昭娩,如果生成的文件過大,就直接當(dāng)做新增來處理黍匾。不過資源文件還需要處理AndroidManifest.xml和resources.arsc這兩個(gè)文件栏渺。

以上是dex,so,resource文件的diff,針對不同的情況和場景有好幾種算法锐涯,只能說微信確實(shí)做到了極致磕诊,厲害了微信。這些算法現(xiàn)在功力不夠纹腌,只能先跳過霎终。另外就是如果看的仔細(xì),還會發(fā)現(xiàn)ApkDecoder中有另外一個(gè)的一個(gè)ArkHotDecoder升薯,這個(gè)貌似是華為的方舟編譯器莱褒。

生成meta,版本文件等

完成了dex,so,resource文件的diff之后涎劈,回到Runner的tinkerPatch方法广凸,還有剩下的產(chǎn)生meta,版本文件和打包成patch文件兩部分。meta文件指的是package_meta.txt蛛枚,這個(gè)文件會將Configuration中一些信息輸出到文件中谅海,以方便在后面進(jìn)行補(bǔ)丁的合成時(shí)進(jìn)行信息校驗(yàn)。生成patch文件的方法在PatchBuilder的buildPatch方法中蹦浦,代碼如下:

// public void buildPatch() throws Exception {
    final File resultDir = config.mTempResultDir;
    if (!resultDir.exists()) {
        throw new IOException(String.format(
            "Missing patch unzip files, path=%s\n", resultDir.getAbsolutePath()));
    }
    //no file change
    if (resultDir.listFiles().length == 0) {
        return;
    }
    generateUnsignedApk(unSignedApk);
    signApk(unSignedApk, signedApk);

    use7zApk(signedApk, signedWith7ZipApk, sevenZipOutPutDir);
    ......
}

第一步在進(jìn)行dex,so,resource文件的diff時(shí)扭吁,將diff文件輸出到目錄下,然后調(diào)用generateUnsignedApk進(jìn)行patch文件的壓縮盲镶,之后對壓縮后的文件再進(jìn)行簽名侥袜。

至此,我們大體上完成了tinker的patch文件的生成溉贿,雖然是囫圇吞棗枫吧,有很多流程也沒有分析。最大的意外是加固模式下dex的算法竟然不是dexDiff顽照,和一直以來想的都不一樣由蘑。但萬里長征第一步,我們大體上了解了patch文件的生成代兵。tinker框架還有g(shù)radle插件部分,以及patch的合成以及加載爷狈。我們后續(xù)再詳細(xì)分析植影。技術(shù)水平有限,有錯誤的地方清不吝指出涎永,感謝思币。

參考文獻(xiàn)

感謝tinker的開源以及先行者的無私分享
Android熱更新開源項(xiàng)目Tinker源碼解析系列之一:Dex熱更新
Tinker源碼分析
Android 熱修復(fù) Tinker 源碼分析之DexDiff / DexPatch
Android動態(tài)資源加載原理和應(yīng)用

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末鹿响,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子谷饿,更是在濱河造成了極大的恐慌惶我,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,548評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件博投,死亡現(xiàn)場離奇詭異绸贡,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)毅哗,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,497評論 3 399
  • 文/潘曉璐 我一進(jìn)店門听怕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人虑绵,你說我怎么就攤上這事尿瞭。” “怎么了翅睛?”我有些...
    開封第一講書人閱讀 167,990評論 0 360
  • 文/不壞的土叔 我叫張陵声搁,是天一觀的道長。 經(jīng)常有香客問我捕发,道長酥艳,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,618評論 1 296
  • 正文 為了忘掉前任爬骤,我火速辦了婚禮充石,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘霞玄。我一直安慰自己骤铃,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,618評論 6 397
  • 文/花漫 我一把揭開白布坷剧。 她就那樣靜靜地躺著惰爬,像睡著了一般。 火紅的嫁衣襯著肌膚如雪惫企。 梳的紋絲不亂的頭發(fā)上撕瞧,一...
    開封第一講書人閱讀 52,246評論 1 308
  • 那天,我揣著相機(jī)與錄音狞尔,去河邊找鬼丛版。 笑死,一個(gè)胖子當(dāng)著我的面吹牛偏序,可吹牛的內(nèi)容都是我干的页畦。 我是一名探鬼主播,決...
    沈念sama閱讀 40,819評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼研儒,長吁一口氣:“原來是場噩夢啊……” “哼豫缨!你這毒婦竟也來了独令?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,725評論 0 276
  • 序言:老撾萬榮一對情侶失蹤好芭,失蹤者是張志新(化名)和其女友劉穎燃箭,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體舍败,經(jīng)...
    沈念sama閱讀 46,268評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡招狸,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,356評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了瓤湘。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瓢颅。...
    茶點(diǎn)故事閱讀 40,488評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖弛说,靈堂內(nèi)的尸體忽然破棺而出挽懦,到底是詐尸還是另有隱情,我是刑警寧澤木人,帶...
    沈念sama閱讀 36,181評論 5 350
  • 正文 年R本政府宣布信柿,位于F島的核電站,受9級特大地震影響醒第,放射性物質(zhì)發(fā)生泄漏渔嚷。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,862評論 3 333
  • 文/蒙蒙 一稠曼、第九天 我趴在偏房一處隱蔽的房頂上張望形病。 院中可真熱鬧,春花似錦霞幅、人聲如沸漠吻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,331評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽途乃。三九已至,卻和暖如春扔傅,著一層夾襖步出監(jiān)牢的瞬間耍共,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,445評論 1 272
  • 我被黑心中介騙來泰國打工猎塞, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留试读,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,897評論 3 376
  • 正文 我出身青樓邢享,卻偏偏與公主長得像鹏往,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子骇塘,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,500評論 2 359