【原】Android热更新开源项目Tinker源码解析系列之一:Dex热更新
【原】Android热更新开源项目Tinker源码解析系列之一:Dex热更新
Tinker是微信的第一个开源项目,主要用于安卓应用bug的热修复和功能的迭代。
Tinker github地址:https://github.com/Tencent/tinker
首先向微信致敬,感谢毫无保留的开源出了这么一款优秀的热更新项目。
因Tinker支持Dex,资源文件及so文件的热更新,本系列将从以下三个方面对Tinker进行源码解析:
- Android热更新开源项目Tinker源码解析系列之一:Dex热更新
- Android热更新开源项目Tinker源码解析系列之二:资源热更新
- Android热更新开源项目Tinker源码解析系类之三:so热更新
Tinker中Dex的热更新也主要分为三个部分,本文也将从这三个方面进行分析:
- 生成补丁流程
- 补丁包下发成功后合成全量Dex流程
- 生成全量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中对资源文件的热更新进行分析。