星空网 > 软件开发 > 操作系统

【原】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 patch12 PatchBuilder builder = new PatchBuilder(config);13 builder.buildPatch();

 

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

会先对manifest文件进行检测,看其是否有更改,如果发现manifest的组件有新增,则抛出异常,因为目前Tinker暂不支持四大组件的新增。

检测通过后解压apk文件,遍历新旧apk,交给ApkFilesVisitor进行处理。

1 //check manifest change first2 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 unchanged16     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 unchanged30     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.tryRecoverDexFiles2 BsDiffPatchInternal.tryRecoverLibraryFiles3 ResDiffPatchInternal.tryRecoverResourceFiles4 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, extraInfoFile100   );101   this.classDefSectionPatchAlg = new ClassDefSectionPatchAlgorithm(102       patchFile, oldDex, patchedDex, oldToFullPatchedIndexMap,103       patchedToSmallPatchedIndexMap, extraInfoFile104   );105   this.typeListSectionPatchAlg = new TypeListSectionPatchAlgorithm(106       patchFile, oldDex, patchedDex, oldToFullPatchedIndexMap,107       patchedToSmallPatchedIndexMap, extraInfoFile108   );109   this.annotationSetRefListSectionPatchAlg = new AnnotationSetRefListSectionPatchAlgorithm(110       patchFile, oldDex, patchedDex, oldToFullPatchedIndexMap,111       patchedToSmallPatchedIndexMap, extraInfoFile112   );113   this.annotationSetSectionPatchAlg = new AnnotationSetSectionPatchAlgorithm(114       patchFile, oldDex, patchedDex, oldToFullPatchedIndexMap,115       patchedToSmallPatchedIndexMap, extraInfoFile116   );117   this.classDataSectionPatchAlg = new ClassDataSectionPatchAlgorithm(118       patchFile, oldDex, patchedDex, oldToFullPatchedIndexMap,119       patchedToSmallPatchedIndexMap, extraInfoFile120   );121   this.codeSectionPatchAlg = new CodeSectionPatchAlgorithm(122       patchFile, oldDex, patchedDex, oldToFullPatchedIndexMap,123       patchedToSmallPatchedIndexMap, extraInfoFile124   );125   this.debugInfoSectionPatchAlg = new DebugInfoItemSectionPatchAlgorithm(126       patchFile, oldDex, patchedDex, oldToFullPatchedIndexMap,127       patchedToSmallPatchedIndexMap, extraInfoFile128   );129   this.annotationSectionPatchAlg = new AnnotationSectionPatchAlgorithm(130       patchFile, oldDex, patchedDex, oldToFullPatchedIndexMap,131       patchedToSmallPatchedIndexMap, extraInfoFile132   );133   this.encodedArraySectionPatchAlg = new StaticValueSectionPatchAlgorithm(134       patchFile, oldDex, patchedDex, oldToFullPatchedIndexMap,135       patchedToSmallPatchedIndexMap, extraInfoFile136   );137   this.annotationsDirectorySectionPatchAlg = new AnnotationsDirectorySectionPatchAlgorithm(138       patchFile, oldDex, patchedDex, oldToFullPatchedIndexMap,139       patchedToSmallPatchedIndexMap, extraInfoFile140   );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     } else26     if (replaceActionCursor < replacedItemCount && replacedIndices[replaceActionCursor] == patchedIndex) {27       T replacedItem = nextItem(patchFile.getBuffer());28       int patchedOffset = writePatchedItem(replacedItem);29       ++replaceActionCursor;30       ++patchedIndex;31     } else32     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     } else42     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     } else51     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           patchedOffset62       );63 64       ++oldIndex;65       ++patchedIndex;66     }67   }68 69   if (addActionCursor != addedItemCount || deletedItemCounter != deletedItemCount70       || replaceActionCursor != replacedItemCount71   ) {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             replacedItemCount83         )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 code17     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 dex11   // dalvik虚拟机中,忽略掉只支持art的dex12   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 file23       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       @Override43       public void onSuccess(File dexFile, File optimizedDir) {44         // Do nothing.45       }46       @Override47       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 // 加载Dex62 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 done19     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 dex24       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;@Overrideprotected 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中对资源文件的热更新进行分析。

 




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

关键词:Android

*特别声明:以上内容来自于网络收集,著作权属原作者所有,如有侵权,请联系我们: admin#shaoqun.com (#换成@)。

艾杰美:https://www.ikjzd.com/w/4521
亚太经济合作组织(APEC):https://www.ikjzd.com/w/4522
自由贸易协定(FTA):https://www.ikjzd.com/w/4523
最惠国待遇:https://www.ikjzd.com/w/4524
国家发展改革委员会:https://www.ikjzd.com/w/4525
Truefitt & Hill:https://www.ikjzd.com/w/4526
建水县住宿推荐:https://www.vstour.cn/a/410233.html
豪华游轮上海到深圳 游轮豪华游深圳出发:https://www.vstour.cn/a/410234.html
相关文章
我的浏览记录
最新相关资讯
海外公司注册 | 跨境电商服务平台 | 深圳旅行社 | 东南亚物流