Tinker的patch生成过程

作者: David_zhou | 来源:发表于2020-03-08 22:11 被阅读0次

    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的差分是微信自己实现的一套dexdiff算法,差分的算法代码主要在tinker-commons中,其中DexPatchApplier是进行生成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是为了验证dex的加载是否成功,test.dex中含有com.tencent.tinker.loader.TinkerTestDexLoad类,该类中包含一个字段isPatch,补丁的加载校验是在SystemClassLoaderAdder中的checkDexInstall方法。
    checkDexInstall就是通过findField该字段判断是否加载成功。

    需要注意的是这个test.dex是从目录下直接拷贝的,而不是直接生成的,test.dex中只有一个TinkerTestDexLoad类,而且其中的属性isPatch是true。因此如果没有加载patch,就会直接加载打包进apk中的TinkerTestDexLoad,此时的isPatch属性为false.如果加载补丁成功,就会从patch中的class.dex中读取TinkerTestDexLoad这个类,而class.dex中TinkerTestDexLoad的属性isPatch是true。因此可以使用读取patch的值来作为补丁是否加载成功的依据。

    而changed_classes.dex可能因为改动代码的范围而生成多个changed_class。 根据不同的情况,最多有四个文件是以meta.txt结尾的:

    package_meta.txt 补丁包的基本信息
    dex_meta.txt dex补丁的信息
    so_meta.txt so补丁的信息
    res_meta.txt 资源补丁的信息
    

    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。其中调用了多个task,名字及类型如下所示:

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

    我们先重点看下tinkerPatch${variantName}这个task,这个task是TinkerPatchSchemaTask类型,在TinkerPatchPlugin的setPatchNewApkPath方法中有如下一句代码:

    tinkerPatchBuildTask.dependsOn variant.assemble
    

    因此在对应的variant执行完assemble之后,就会执行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的参数,从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);
    }
    

    这个方法首先从tinker.gradle中读取相关配置,然后作为参数,开始调用Runner.gradleRun方法开始准备生成patch文件。gradleRun中调用来run方法,run中直接调用来tinkerPatch, 这个方法就真正开始创建patch。下面我们看下这个方法,代码如下:

    // 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文件的生成

    下面我们先重点看下patch的第一部分的生成。 ApkDecoder的构造方法如下:

    // 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类型的多个Decoder,在构造方法将这几个成员变量初始化。 各个Decoder分别针对代码中不同的部分进行patch。都是继承自BaseDecoder,然后实现了patch,onAllPatchesStart,onAllPatchesEnd这个三个抽象方法。 创建了ApkDecoder实例decoder后,调用onAllPatchesStart进行patch之前的准备工作。onAllPatchesStart代码如下:

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

    随后每个decoder分别执行自己的onAllPatchesStart方法。调用onAllPatchesStart方法后,就调用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文件的改动。然后调用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变化的处理。
        }
    }
    

    这部分的逻辑是先调用AndroidParser的getAndroidManifest方法从文件中读取到manifest的内容,然后拿出其中的xml进行比较。如果没有变化,直接返回。一般情况下manifest的变化都是新增四大组件导致的, 而热更比较少新增,因此manifest变化的情况先跳过。另外manifest的修改了,后续进行patch的合成加载进行相应的处理。 回到ApkDecoder的patch方法继续看,将要打包patch的两个apk解压后。然后调用Files的walkFileTree方法来遍历新的apk解压后的目录。这个Files是NIO中的类,walkFileTree方法传递两个参数,一个是 要遍历的目录,第二个参数是遍历行为控制器FileVisitor,它是一个接口,里面定义了4个方法用来指定当你访问一个节点之前、之中、之后、失败时应该采取什么行动。这里看下ApkFilesVisitor这个类,实现了visitFile方法,制定访问文件时的操作行为。的构造方法代码如下:

    // 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构造方法中初始化的各种decoder传递进来,在遍历目录针对不同类型的文件调用不同的decoder。下面看下遍历文件visitFile这个方法,代码如下:

    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命名规则的文件调用UniqueDexDiffDecoder进行patch,对符合config.mSoFilePattern命名规则的文件调用BsDiffDecoder进行patch,对符合config.mResFilePattern命名要求的文件调用ResDiffDecoder进行patch合成。mDexFilePattern,mSoFilePattern,mResFilePattern这结果值是在tinker.gradle中进行的配置,一般使用默认就行。下面先对UniqueDexDiffDecoder的patch进行分析,从名字上来看,我们离差分操作不远了。

    dex的差分

    dex文件的查分操作在UniqueDexDiffDecoder的patch方法中,UniqueDexDiffDecoder继承自DexDiffDecoder。UniqueDexDiffDecoder的patch操作主要是调用父类的patch操作,之后对文件名进行重名判断。我们主要看下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这个方法主要是做一些准备工作,准备工作很繁琐,需要处理的情况真多。而进行差分工作主要在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,则产生的package会包含所有的改动文件。而对于非加固的文件,只需要进行dexdiff算法后生成的patch-info文件。这部分的说明来自TinkerBuildConfigExtension中对于的isProtectedApp的注释。generateChangedClassesDexFile这个方法使用的是dexlib2库中的DexBuilder来生成dex文件。而generatePatchInfoFile这个方法使用的是DexPatchApplier,这个类是使用微信自研的dexDiff算法。对于这个算法,简单的介绍和示范在Android 热修复 Tinker 源码分析之DexDiff / DexPatch,全面详细的介绍在DexDiff。这个地方其实有点意外,因为一直以为dex的patch是使用微信自研的dexDiff算法,没想到加固的APP并不是这套算法,而是dexlib2这个框架。dex的patch这部分看了一天,看得很晕,感谢厉害的大佬们,以及更厉害的微信。

    so的差分

    so的patch是在BsDiffDecoder的patch中进行,

    // 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进行diff操作,这是二进制的差量算法,具体的介绍在这里BSDiff算法. 如果BSDiff.bsdiff方法生成的文件过大,就会直接当做新增加的文件来对待,以避免patch时间过长。

    resource的差分

    资源文件的diff在ResDiffDecoder这个类的patch方法中完成,采用的方法也是BSDiff算法。牵扯到知识真多,真是功夫在诗外。和so一样,如果新增,则直接拷贝。否则调用BSDiff算法,如果生成的文件过大,就直接当做新增来处理。不过资源文件还需要处理AndroidManifest.xml和resources.arsc这两个文件。

    以上是dex,so,resource文件的diff,针对不同的情况和场景有好几种算法,只能说微信确实做到了极致,厉害了微信。这些算法现在功力不够,只能先跳过。另外就是如果看的仔细,还会发现ApkDecoder中有另外一个的一个ArkHotDecoder,这个貌似是华为的方舟编译器。

    生成meta,版本文件等

    完成了dex,so,resource文件的diff之后,回到Runner的tinkerPatch方法,还有剩下的产生meta,版本文件和打包成patch文件两部分。meta文件指的是package_meta.txt,这个文件会将Configuration中一些信息输出到文件中,以方便在后面进行补丁的合成时进行信息校验。生成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);
        ......
    }
    

    第一步在进行dex,so,resource文件的diff时,将diff文件输出到目录下,然后调用generateUnsignedApk进行patch文件的压缩,之后对压缩后的文件再进行签名。

    至此,我们大体上完成了tinker的patch文件的生成,虽然是囫囵吞枣,有很多流程也没有分析。最大的意外是加固模式下dex的算法竟然不是dexDiff,和一直以来想的都不一样。但万里长征第一步,我们大体上了解了patch文件的生成。tinker框架还有gradle插件部分,以及patch的合成以及加载。我们后续再详细分析。技术水平有限,有错误的地方清不吝指出,感谢。

    参考文献

    感谢tinker的开源以及先行者的无私分享
    Android热更新开源项目Tinker源码解析系列之一:Dex热更新
    Tinker源码分析
    Android 热修复 Tinker 源码分析之DexDiff / DexPatch
    Android动态资源加载原理和应用

    相关文章

      网友评论

        本文标题:Tinker的patch生成过程

        本文链接:https://www.haomeiwen.com/subject/trksdhtx.html