zoukankan      html  css  js  c++  java
  • 【原】Android热更新开源项目Tinker源码解析系列之一:Dex热更新

    【原】Android热更新开源项目Tinker源码解析系列之一:Dex热更新

     

    Tinker是微信的第一个开源项目,主要用于安卓应用bug的热修复和功能的迭代。

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

    首先向微信致敬,感谢毫无保留的开源出了这么一款优秀的热更新项目。

    因Tinker支持Dex,资源文件及so文件的热更新,本系列将从以下三个方面对Tinker进行源码解析:

    1. Android热更新开源项目Tinker源码解析系列之一:Dex热更新
    2. Android热更新开源项目Tinker源码解析系列之二:资源热更新
    3. Android热更新开源项目Tinker源码解析系类之三:so热更新

     

    Tinker中Dex的热更新也主要分为三个部分,本文也将从这三个方面进行分析:

    1. 生成补丁流程
    2. 补丁包下发成功后合成全量Dex流程
    3. 生成全量Dex后的加载流程

     

    转载请标明本文来源:http://www.cnblogs.com/yyangblog/p/6249715.html 
    更多内容欢迎star作者的github:https://github.com/LaurenceYang/article
    如果发现本文有什么问题和任何建议,也随时欢迎交流~

     

    一、生成补丁流程

    当在命令行里面调用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。

     1 private static void patchDexFile(
     2         ZipFile baseApk, ZipFile patchPkg, ZipEntry oldDexEntry, ZipEntry patchFileEntry,
     3         ShareDexDiffPatchInfo patchInfo,  File patchedDexFile) throws IOException {
     4     InputStream oldDexStream = null;
     5     InputStream patchFileStream = null;
     6     try {
     7         oldDexStream = baseApk.getInputStream(oldDexEntry);
     8         patchFileStream = (patchFileEntry != null ? patchPkg.getInputStream(patchFileEntry) : null);
     9 
    10         final boolean isRawDexFile = SharePatchFileUtil.isRawDexFile(patchInfo.rawName);
    11         if (!isRawDexFile || patchInfo.isJarMode) {
    12             ZipOutputStream zos = null;
    13             try {
    14                 zos = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(patchedDexFile)));
    15                 zos.putNextEntry(new ZipEntry(ShareConstants.DEX_IN_JAR));
    16                 // Old dex is not a raw dex file.
    17                 if (!isRawDexFile) {
    18                     ZipInputStream zis = null;
    19                     try {
    20                         zis = new ZipInputStream(oldDexStream);
    21                         ZipEntry entry;
    22                         while ((entry = zis.getNextEntry()) != null) {
    23                             if (ShareConstants.DEX_IN_JAR.equals(entry.getName())) break;
    24                         }
    25                         if (entry == null) {
    26                             throw new TinkerRuntimeException("can't recognize zip dex format file:" + patchedDexFile.getAbsolutePath());
    27                         }
    28                         new DexPatchApplier(zis, (int) entry.getSize(), patchFileStream).executeAndSaveTo(zos);
    29                     } finally {
    30                         SharePatchFileUtil.closeQuietly(zis);
    31                     }
    32                 } else {
    33                     new DexPatchApplier(oldDexStream, (int) oldDexEntry.getSize(), patchFileStream).executeAndSaveTo(zos);
    34                 }
    35                 zos.closeEntry();
    36             } finally {
    37                 SharePatchFileUtil.closeQuietly(zos);
    38             }
    39         } else {
    40             new DexPatchApplier(oldDexStream, (int) oldDexEntry.getSize(), patchFileStream).executeAndSaveTo(patchedDexFile);
    41         }
    42     } finally {
    43         SharePatchFileUtil.closeQuietly(oldDexStream);
    44         SharePatchFileUtil.closeQuietly(patchFileStream);
    45     }
    46 }

    DexPatchApplier.executeAndSaveTo(OutputStream out)中会对15个dex区域进行patch操作,

    针对old dex和patch dex进行合并,生成全量dex文件。

      1 public void executeAndSaveTo(OutputStream out) throws IOException {
      2     // Before executing, we should check if this patch can be applied to
      3     // old dex we passed in.
      4     // 首先old apk的签名和patchfile所携带的old apk签名是否一致,不一致则抛出异常
      5     byte[] oldDexSign = this.oldDex.computeSignature(false);
      6     if (oldDexSign == null) {
      7         throw new IOException("failed to compute old dex's signature.");
      8     }
      9 
     10     if (this.patchFile != null) {
     11         byte[] oldDexSignInPatchFile = this.patchFile.getOldDexSignature();
     12         if (CompareUtils.uArrCompare(oldDexSign, oldDexSignInPatchFile) != 0) {
     13             throw new IOException(
     14                     String.format(
     15                             "old dex signature mismatch! expected: %s, actual: %s",
     16                             Arrays.toString(oldDexSign),
     17                             Arrays.toString(oldDexSignInPatchFile)
     18                     )
     19             );
     20         }
     21     }
     22 
     23     String oldDexSignStr = Hex.toHexString(oldDexSign);
     24 
     25     // Firstly, set sections' offset after patched, sort according to their offset so that
     26     // the dex lib of aosp can calculate section size.
     27     // patchedDex是最终合成的dex,首先设定各个区域的偏移量
     28     TableOfContents patchedToc = this.patchedDex.getTableOfContents();
     29 
     30     patchedToc.header.off = 0;
     31     patchedToc.header.size = 1;
     32     patchedToc.mapList.size = 1;
     33 
     34     if (extraInfoFile == null || !extraInfoFile.isAffectedOldDex(this.oldDexSignStr)) {
     35         patchedToc.stringIds.off
     36                 = this.patchFile.getPatchedStringIdSectionOffset();
     37         patchedToc.typeIds.off
     38                 = this.patchFile.getPatchedTypeIdSectionOffset();
     39         patchedToc.typeLists.off
     40                 = this.patchFile.getPatchedTypeListSectionOffset();
     41         patchedToc.protoIds.off
     42                 = this.patchFile.getPatchedProtoIdSectionOffset();
     43         patchedToc.fieldIds.off
     44                 = this.patchFile.getPatchedFieldIdSectionOffset();
     45         patchedToc.methodIds.off
     46                 = this.patchFile.getPatchedMethodIdSectionOffset();
     47         patchedToc.classDefs.off
     48                 = this.patchFile.getPatchedClassDefSectionOffset();
     49         patchedToc.mapList.off
     50                 = this.patchFile.getPatchedMapListSectionOffset();
     51         patchedToc.stringDatas.off
     52                 = this.patchFile.getPatchedStringDataSectionOffset();
     53         patchedToc.annotations.off
     54                 = this.patchFile.getPatchedAnnotationSectionOffset();
     55         patchedToc.annotationSets.off
     56                 = this.patchFile.getPatchedAnnotationSetSectionOffset();
     57         patchedToc.annotationSetRefLists.off
     58                 = this.patchFile.getPatchedAnnotationSetRefListSectionOffset();
     59         patchedToc.annotationsDirectories.off
     60                 = this.patchFile.getPatchedAnnotationsDirectorySectionOffset();
     61         patchedToc.encodedArrays.off
     62                 = this.patchFile.getPatchedEncodedArraySectionOffset();
     63         patchedToc.debugInfos.off
     64                 = this.patchFile.getPatchedDebugInfoSectionOffset();
     65         patchedToc.codes.off
     66                 = this.patchFile.getPatchedCodeSectionOffset();
     67         patchedToc.classDatas.off
     68                 = this.patchFile.getPatchedClassDataSectionOffset();
     69         patchedToc.fileSize
     70                 = this.patchFile.getPatchedDexSize();
     71     } else {
     72        ...
     73     }
     74 
     75     Arrays.sort(patchedToc.sections);
     76 
     77     patchedToc.computeSizesFromOffsets();
     78 
     79     // Secondly, run patch algorithms according to sections' dependencies.
     80     // 对每个区域进行patch操作
     81     this.stringDataSectionPatchAlg = new StringDataSectionPatchAlgorithm(
     82             patchFile, oldDex, patchedDex, oldToFullPatchedIndexMap,
     83             patchedToSmallPatchedIndexMap, extraInfoFile
     84     );
     85     this.typeIdSectionPatchAlg = new TypeIdSectionPatchAlgorithm(
     86             patchFile, oldDex, patchedDex, oldToFullPatchedIndexMap,
     87             patchedToSmallPatchedIndexMap, extraInfoFile
     88     );
     89     this.protoIdSectionPatchAlg = new ProtoIdSectionPatchAlgorithm(
     90             patchFile, oldDex, patchedDex, oldToFullPatchedIndexMap,
     91             patchedToSmallPatchedIndexMap, extraInfoFile
     92     );
     93     this.fieldIdSectionPatchAlg = new FieldIdSectionPatchAlgorithm(
     94             patchFile, oldDex, patchedDex, oldToFullPatchedIndexMap,
     95             patchedToSmallPatchedIndexMap, extraInfoFile
     96     );
     97     this.methodIdSectionPatchAlg = new MethodIdSectionPatchAlgorithm(
     98             patchFile, oldDex, patchedDex, oldToFullPatchedIndexMap,
     99             patchedToSmallPatchedIndexMap, extraInfoFile
    100     );
    101     this.classDefSectionPatchAlg = new ClassDefSectionPatchAlgorithm(
    102             patchFile, oldDex, patchedDex, oldToFullPatchedIndexMap,
    103             patchedToSmallPatchedIndexMap, extraInfoFile
    104     );
    105     this.typeListSectionPatchAlg = new TypeListSectionPatchAlgorithm(
    106             patchFile, oldDex, patchedDex, oldToFullPatchedIndexMap,
    107             patchedToSmallPatchedIndexMap, extraInfoFile
    108     );
    109     this.annotationSetRefListSectionPatchAlg = new AnnotationSetRefListSectionPatchAlgorithm(
    110             patchFile, oldDex, patchedDex, oldToFullPatchedIndexMap,
    111             patchedToSmallPatchedIndexMap, extraInfoFile
    112     );
    113     this.annotationSetSectionPatchAlg = new AnnotationSetSectionPatchAlgorithm(
    114             patchFile, oldDex, patchedDex, oldToFullPatchedIndexMap,
    115             patchedToSmallPatchedIndexMap, extraInfoFile
    116     );
    117     this.classDataSectionPatchAlg = new ClassDataSectionPatchAlgorithm(
    118             patchFile, oldDex, patchedDex, oldToFullPatchedIndexMap,
    119             patchedToSmallPatchedIndexMap, extraInfoFile
    120     );
    121     this.codeSectionPatchAlg = new CodeSectionPatchAlgorithm(
    122             patchFile, oldDex, patchedDex, oldToFullPatchedIndexMap,
    123             patchedToSmallPatchedIndexMap, extraInfoFile
    124     );
    125     this.debugInfoSectionPatchAlg = new DebugInfoItemSectionPatchAlgorithm(
    126             patchFile, oldDex, patchedDex, oldToFullPatchedIndexMap,
    127             patchedToSmallPatchedIndexMap, extraInfoFile
    128     );
    129     this.annotationSectionPatchAlg = new AnnotationSectionPatchAlgorithm(
    130             patchFile, oldDex, patchedDex, oldToFullPatchedIndexMap,
    131             patchedToSmallPatchedIndexMap, extraInfoFile
    132     );
    133     this.encodedArraySectionPatchAlg = new StaticValueSectionPatchAlgorithm(
    134             patchFile, oldDex, patchedDex, oldToFullPatchedIndexMap,
    135             patchedToSmallPatchedIndexMap, extraInfoFile
    136     );
    137     this.annotationsDirectorySectionPatchAlg = new AnnotationsDirectorySectionPatchAlgorithm(
    138             patchFile, oldDex, patchedDex, oldToFullPatchedIndexMap,
    139             patchedToSmallPatchedIndexMap, extraInfoFile
    140     );
    141 
    142     this.stringDataSectionPatchAlg.execute();
    143     this.typeIdSectionPatchAlg.execute();
    144     this.typeListSectionPatchAlg.execute();
    145     this.protoIdSectionPatchAlg.execute();
    146     this.fieldIdSectionPatchAlg.execute();
    147     this.methodIdSectionPatchAlg.execute();
    148     Runtime.getRuntime().gc();
    149     this.annotationSectionPatchAlg.execute();
    150     this.annotationSetSectionPatchAlg.execute();
    151     this.annotationSetRefListSectionPatchAlg.execute();
    152     this.annotationsDirectorySectionPatchAlg.execute();
    153     Runtime.getRuntime().gc();
    154     this.debugInfoSectionPatchAlg.execute();
    155     this.codeSectionPatchAlg.execute();
    156     Runtime.getRuntime().gc();
    157     this.classDataSectionPatchAlg.execute();
    158     this.encodedArraySectionPatchAlg.execute();
    159     this.classDefSectionPatchAlg.execute();
    160     Runtime.getRuntime().gc();
    161 
    162     // Thirdly, write header, mapList. Calculate and write patched dex's sign and checksum.
    163     Dex.Section headerOut = this.patchedDex.openSection(patchedToc.header.off);
    164     patchedToc.writeHeader(headerOut);
    165 
    166     Dex.Section mapListOut = this.patchedDex.openSection(patchedToc.mapList.off);
    167     patchedToc.writeMap(mapListOut);
    168 
    169     this.patchedDex.writeHashes();
    170 
    171     // Finally, write patched dex to file.
    172     this.patchedDex.writeTo(out);

    每个区域的合并算法采用二路归并,在old dex的基础上对元素进行删除,增加,替换操作。

    这里的算法和生成补丁的DexDiff是一个逆向的过程。

     1 private void doFullPatch(
     2         Dex.Section oldSection,
     3         int oldItemCount,
     4         int[] deletedIndices,
     5         int[] addedIndices,
     6         int[] replacedIndices
     7 ) {
     8     int deletedItemCount = deletedIndices.length;
     9     int addedItemCount = addedIndices.length;
    10     int replacedItemCount = replacedIndices.length;
    11     int newItemCount = oldItemCount + addedItemCount - deletedItemCount;
    12 
    13     int deletedItemCounter = 0;
    14     int addActionCursor = 0;
    15     int replaceActionCursor = 0;
    16 
    17     int oldIndex = 0;
    18     int patchedIndex = 0;
    19     while (oldIndex < oldItemCount || patchedIndex < newItemCount) {
    20         if (addActionCursor < addedItemCount && addedIndices[addActionCursor] == patchedIndex) {
    21             T addedItem = nextItem(patchFile.getBuffer());
    22             int patchedOffset = writePatchedItem(addedItem);
    23             ++addActionCursor;
    24             ++patchedIndex;
    25         } else
    26         if (replaceActionCursor < replacedItemCount && replacedIndices[replaceActionCursor] == patchedIndex) {
    27             T replacedItem = nextItem(patchFile.getBuffer());
    28             int patchedOffset = writePatchedItem(replacedItem);
    29             ++replaceActionCursor;
    30             ++patchedIndex;
    31         } else
    32         if (Arrays.binarySearch(deletedIndices, oldIndex) >= 0) {
    33             T skippedOldItem = nextItem(oldSection); // skip old item.
    34             markDeletedIndexOrOffset(
    35                     oldToFullPatchedIndexMap,
    36                     oldIndex,
    37                     getItemOffsetOrIndex(oldIndex, skippedOldItem)
    38             );
    39             ++oldIndex;
    40             ++deletedItemCounter;
    41         } else
    42         if (Arrays.binarySearch(replacedIndices, oldIndex) >= 0) {
    43             T skippedOldItem = nextItem(oldSection); // skip old item.
    44             markDeletedIndexOrOffset(
    45                     oldToFullPatchedIndexMap,
    46                     oldIndex,
    47                     getItemOffsetOrIndex(oldIndex, skippedOldItem)
    48             );
    49             ++oldIndex;
    50         } else
    51         if (oldIndex < oldItemCount) {
    52             T oldItem = adjustItem(this.oldToFullPatchedIndexMap, nextItem(oldSection));
    53 
    54             int patchedOffset = writePatchedItem(oldItem);
    55 
    56             updateIndexOrOffset(
    57                     this.oldToFullPatchedIndexMap,
    58                     oldIndex,
    59                     getItemOffsetOrIndex(oldIndex, oldItem),
    60                     patchedIndex,
    61                     patchedOffset
    62             );
    63 
    64             ++oldIndex;
    65             ++patchedIndex;
    66         }
    67     }
    68 
    69     if (addActionCursor != addedItemCount || deletedItemCounter != deletedItemCount
    70             || replaceActionCursor != replacedItemCount
    71     ) {
    72         throw new IllegalStateException(
    73                 String.format(
    74                         "bad patch operation sequence. addCounter: %d, addCount: %d, "
    75                                 + "delCounter: %d, delCount: %d, "
    76                                 + "replaceCounter: %d, replaceCount:%d",
    77                         addActionCursor,
    78                         addedItemCount,
    79                         deletedItemCounter,
    80                         deletedItemCount,
    81                         replaceActionCursor,
    82                         replacedItemCount
    83                 )
    84         );
    85     }
    86 }

    在extractDexDiffInternals调用完以后,

    会调用TinkerParallelDexOptimizer.optimizeAll对生成的全量dex进行optimize操作,生成odex文件。

    最终合成的文件会放到/data/data/${package_name}/tinker目录下。

    到此,生成Dex过程完成。

     

    三、加载全量Dex流程

    TinkerApplication通过反射的方式将实际的app业务隔离,这样可以在热更新的时候修改实际的app内容。

    在TinkerApplication中的onBaseContextAttached中会通过反射调用TinkerLoader的tryLoad加载已经合成的dex。

     1 private static final String TINKER_LOADER_METHOD   = "tryLoad";
     2 private void loadTinker() {
     3     //disable tinker, not need to install
     4     if (tinkerFlags == TINKER_DISABLE) {
     5         return;
     6     }
     7     tinkerResultIntent = new Intent();
     8     try {
     9         //reflect tinker loader, because loaderClass may be define by user!
    10         Class<?> tinkerLoadClass = Class.forName(loaderClassName, false, getClassLoader());
    11 
    12         Method loadMethod = tinkerLoadClass.getMethod(TINKER_LOADER_METHOD, TinkerApplication.class, int.class, boolean.class);
    13         Constructor<?> constructor = tinkerLoadClass.getConstructor();
    14         tinkerResultIntent = (Intent) loadMethod.invoke(constructor.newInstance(), this, tinkerFlags, tinkerLoadVerifyFlag);
    15     } catch (Throwable e) {
    16         //has exception, put exception error code
    17         ShareIntentUtil.setIntentReturnCode(tinkerResultIntent, ShareConstants.ERROR_LOAD_PATCH_UNKNOWN_EXCEPTION);
    18         tinkerResultIntent.putExtra(INTENT_PATCH_EXCEPTION, e);
    19     }
    20 }

    tryLoadPatchFilesInternal是加载Patch文件的核心函数,主要做了以下的事情:

    • tinkerFlag是否开启,否则不加载
    • tinker目录是否生成,没有则表示没有生成全量的dex,不需要重新加载
    • tinker目录是否生成,没有则表示没有生成全量的dex,不需要重新加载
    • tinker/patch.info是否存在,否则不加载
    • 读取patch.info,读取失败则不加载
    • 比较patchInfo的新旧版本,都为空则不加载
    • 判断版本号是否为空,为空则不加载
    • 判断patch version directory(//tinker/patch.info/patch-641e634c)是否存在
    • 判断patchVersionDirectoryFile(//tinker/patch.info/patch-641e634c/patch-641e634c.apk)是否存在
    • checkTinkerPackage,(如tinkerId和oldTinkerId不能相等,否则不加载)
    • 检测dex的完整性,包括dex是否全部生产,是否对dex做了优化,优化后的文件是否存在(//tinker/patch.info/patch-641e634c/dex)
    • 同样对so res文件进行完整性检测
    • 尝试超过3次不加载
    • loadTinkerJars/loadTinkerResources/

    TinkerDexLoader.loadTinkerJars处理加载dex文件。

     1 // 获取PatchClassLoader 
     2 PathClassLoader classLoader = (PathClassLoader) TinkerDexLoader.class.getClassLoader();
     3 
     4 ...
     5 // 生产合法文件列表
     6 ArrayList<File> legalFiles = new ArrayList<>();
     7 
     8 final boolean isArtPlatForm = ShareTinkerInternals.isVmArt();
     9 for (ShareDexDiffPatchInfo info : dexList) {
    10     //for dalvik, ignore art support dex
    11     // dalvik虚拟机中,忽略掉只支持art的dex
    12     if (isJustArtSupportDex(info)) {
    13         continue;
    14     }
    15     String path = dexPath + info.realName;
    16     File file = new File(path);
    17 
    18     if (tinkerLoadVerifyFlag) {
    19         long start = System.currentTimeMillis();
    20         String checkMd5 = isArtPlatForm ? info.destMd5InArt : info.destMd5InDvm;
    21         if (!SharePatchFileUtil.verifyDexFileMd5(file, checkMd5)) {
    22             //it is good to delete the mismatch file
    23             ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_MD5_MISMATCH);
    24             intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_MISMATCH_DEX_PATH,
    25                 file.getAbsolutePath());
    26             return false;
    27         }
    28         Log.i(TAG, "verify dex file:" + file.getPath() + " md5, use time: " + (System.currentTimeMillis() - start));
    29     }
    30     legalFiles.add(file);
    31 }
    32 
    33 // 如果系统OTA,对这些合法dex进行优化
    34 if (isSystemOTA) {
    35     parallelOTAResult = true;
    36     parallelOTAThrowable = null;
    37     Log.w(TAG, "systemOTA, try parallel oat dexes!!!!!");
    38 
    39     TinkerParallelDexOptimizer.optimizeAll(
    40         legalFiles, optimizeDir,
    41         new TinkerParallelDexOptimizer.ResultCallback() {
    42             @Override
    43             public void onSuccess(File dexFile, File optimizedDir) {
    44                 // Do nothing.
    45             }
    46             @Override
    47             public void onFailed(File dexFile, File optimizedDir, Throwable thr) {
    48                 parallelOTAResult = false;
    49                 parallelOTAThrowable = thr;
    50             }
    51         }
    52     );
    53     if (!parallelOTAResult) {
    54         Log.e(TAG, "parallel oat dexes failed");
    55         intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, parallelOTAThrowable);
    56         ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_PARALLEL_DEX_OPT_EXCEPTION);
    57         return false;
    58     }
    59 }
    60 
    61 // 加载Dex
    62 SystemClassLoaderAdder.installDexes(application, classLoader, optimizeDir, legalFiles);

    SystemClassLoaderAdder.installDexes中按照安卓的版本对dex进行install,这里应该是借鉴了MultiDex里面的install做法。

    另外Tinker在生成补丁阶段会生成一个test.dex,这个test.dex的作用就是用来验证dex的加载是否成功。

    test.dex中含有com.tencent.tinker.loader.TinkerTestDexLoad类,该类中包含一个字段isPatch,checkDexInstall就是通过findField该字段判断是否加载成功。

     1 public static void installDexes(Application application, PathClassLoader loader, File dexOptDir, List<File> files) throws Throwable {
     2     if (!files.isEmpty()) {
     3         ClassLoader classLoader = loader;
     4         if (Build.VERSION.SDK_INT >= 24) {
     5             classLoader = AndroidNClassLoader.inject(loader, application);
     6         }
     7         //because in dalvik, if inner class is not the same classloader with it wrapper class.
     8         //it won't fail at dex2opt
     9         if (Build.VERSION.SDK_INT >= 23) {
    10             V23.install(classLoader, files, dexOptDir);
    11         } else if (Build.VERSION.SDK_INT >= 19) {
    12             V19.install(classLoader, files, dexOptDir);
    13         } else if (Build.VERSION.SDK_INT >= 14) {
    14             V14.install(classLoader, files, dexOptDir);
    15         } else {
    16             V4.install(classLoader, files, dexOptDir);
    17         }
    18         //install done
    19         sPatchDexCount = files.size();
    20     
    21         // Tinker在生成补丁阶段会生成一个test.dex,这个test.dex的作用就是用来验证dex的加载是否成功。test.dex中含有com.tencent.tinker.loader.TinkerTestDexLoad类,该类中包含一个字段isPatch,checkDexInstall就是通过findField该字段判断是否加载成功。
    22         if (!checkDexInstall(classLoader)) {
    23             //reset patch dex
    24             SystemClassLoaderAdder.uninstallPatchDex(classLoader);
    25             throw new TinkerRuntimeException(ShareConstants.CHECK_DEX_INSTALL_FAIL);
    26         }
    27     }
    28 }

    ------分割线-----

    在讲install具体细节之前,回顾一下具体原理。关于Android的ClassLoader体系,android中加载类一般使用的是PathClassLoader和DexClassLoader

    PathClassLoader,源码注释可以看出,android使用这个类作为系统类和应用类的加载器。

    /**
     * Provides a simple {@link ClassLoader} implementation that operates on a list
     * of files and directories in the local file system, but does not attempt to
     * load classes from the network. Android uses this class for its system class
     * loader and for its application class loader(s).
     */

    DexClassLoader,源码注释可以看出,可以用来从.jar和.apk类型的文件内部加载classes.dex文件。

    /**
     * A class loader that loads classes from {@code .jar} and {@code .apk} files
     * containing a {@code classes.dex} entry. This can be used to execute code not
     * installed as part of an application.
     *
     * <p>This class loader requires an application-private, writable directory to
     * cache optimized classes. Use {@code Context.getDir(String, int)} to create
     * such a directory: <pre>   {@code
     *   File dexOutputDir = context.getDir("dex", 0);
     * }</pre>
     *
     * <p><strong>Do not cache optimized classes on external storage.</strong>
     * External storage does not provide access controls necessary to protect your
     * application from code injection attacks.
     */

    ok,到这里,大家只需要明白,Android使用PathClassLoader作为其类加载器,DexClassLoader可以从.jar和.apk类型的文件内部加载classes.dex文件就好了。

    PathClassLoader和DexClassLoader都继承自BaseDexClassLoader。在BaseDexClassLoader中有如下源码:

    ##BaseDexClassLoader.java##
    /** structured lists of path elements */
    private final DexPathList pathList;
    
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class clazz = pathList.findClass(name);
        if (clazz == null) {
            throw new ClassNotFoundException(name);
        }
        return clazz;
    }
    
    ##DexPathList.java##
    /** list of dex/resource (class path) elements */
    private final Element[] dexElements;
    public Class findClass(String name) {
        for (Element element : dexElements) {
            DexFile dex = element.dexFile;
            if (dex != null) {
                Class clazz = dex.loadClassBinaryName(name, definingContext);
                if (clazz != null) {
                    return clazz;
                }
            }
        }
        return null;
    }
    
    ##DexFile.java##
    public Class loadClassBinaryName(String name, ClassLoader loader) {
        return defineClass(name, loader, mCookie);
    }
    private native static Class defineClass(String name, ClassLoader loader, int cookie);

    通俗点讲:

    一个ClassLoader可以包含多个dex文件,每个dex文件是一个Element,多个dex文件排列成一个有序的数组dexElements,当找类的时候,会按顺序遍历dex文件,然后从当前遍历的dex文件中找类,如果找类则返回,如果找不到从下一个dex文件继续查找。(来自:安卓App热补丁动态修复技术介绍)

     

    回到分割线以前:

    install的做法就是,先获取BaseDexClassLoader的dexPathList对象,

    然后通过dexPathList的makeDexElements函数将我们要安装的dex转化成Element[]对象,

    最后将其和dexPathList的dexElements对象进行合并,就是新的Element[]对象,

    因为我们添加的dex都被放在dexElements数组的最前面,所以当通过findClass来查找这个类时,就是使用的我们最新的dex里面的类。

    以V19的install为例,下面的代码非常清晰的描述了实际的加载所做的事情:

     1 private static final class V19 {
     2     private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
     3                                 File optimizedDirectory)
     4         throws IllegalArgumentException, IllegalAccessException,
     5         NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
     6         /* The patched class loader is expected to be a descendant of
     7          * dalvik.system.BaseDexClassLoader. We modify its
     8          * dalvik.system.DexPathList pathList field to append additional DEX
     9          * file entries.
    10          */
    11         Field pathListField = ShareReflectUtil.findField(loader, "pathList");
    12         Object dexPathList = pathListField.get(loader);
    13         ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
    14         ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
    15             new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
    16             suppressedExceptions));
    17         if (suppressedExceptions.size() > 0) {
    18             for (IOException e : suppressedExceptions) {
    19                 Log.w(TAG, "Exception in makeDexElement", e);
    20                 throw e;
    21             }
    22         }
    23     }
    24 }

    因为android版本更新较快,不同版本里面的DexPathList等类的函数和字段都有一些变化,这也是在install的时候需要对不同版本进行适配的原因。

    到此,在当前app的classloader里面就包含了我们第二步骤里面合成的全量DEX,我们在加载类的时候就能用到新的内容了。

    Congratulations!!Dex的加载流程完成。


    转载请标明本文来源:http://www.cnblogs.com/yyangblog/p/6249715.html 
    更多内容欢迎star作者的github:https://github.com/LaurenceYang/article
    如果发现本文有什么问题和任何建议,也随时欢迎交流~

     

    下一篇文章我们将对Tinker中对资源文件的热更新进行分析。

  • 相关阅读:
    Kubernetes 1.5部署sonarqube
    Kubernetes 1.5集成heapster
    Kubernetes 1.5 配置dashboard
    SQL SERVER中的逻辑读取,物理读取,以及预读的理解
    JS控制显示/隐藏二级菜单
    Css下拉菜单设置
    div包裹页面后多余部分没有显示,也没滚动条 overflow 属性设置
    Sql Ado.net 学习笔记之连接字符串
    Winform异步解决窗体耗时操作(Action专门用于无返回值,Func专门用于有返回值)
    SQL中的字母的大小写转换
  • 原文地址:https://www.cnblogs.com/yyangblog/p/6249715.html
Copyright © 2011-2022 走看看