Tinker源码分析
来源:互联网 发布:linux grep -ef|ps 编辑:程序博客网 时间:2024/06/06 12:52
Tinker撸码
APK Diff 生成补丁
Tinker接入方式有两种:gradle插件和命令行。
两者只是配置参数输入源不同,前者是读取gradle文件,后者是读取命令行参数。
入口类分别是
//com.tencent.tinker.build.gradle.task.TinkerPatchSchemaTask @TaskAction def tinkerPatch() { ... InputParam.Builder builder = new InputParam.Builder() builder.setOldApk(configuration.oldApk) .setNewApk(buildApkPath) .setOutBuilder(outputFolder) .setIgnoreWarning(configuration.ignoreWarning) .setDexFilePattern(new ArrayList<String>(configuration.dex.pattern)) .setDexLoaderPattern(new ArrayList<String>(configuration.dex.loader)) .setDexMode(configuration.dex.dexMode) .setSoFilePattern(new ArrayList<String>(configuration.lib.pattern)) .setResourceFilePattern(new ArrayList<String>(configuration.res.pattern)) .setResourceIgnoreChangePattern(new ArrayList<String>(configuration.res.ignoreChange)) .setResourceLargeModSize(configuration.res.largeModSize) .setUseApplyResource(configuration.buildConfig.usingResourceMapping) .setConfigFields(new HashMap<String, String>(configuration.packageConfig.getFields())) .setSevenZipPath(configuration.sevenZip.path) .setUseSign(configuration.useSign) InputParam inputParam = builder.create() //调用Runner的静态方法,InputParam是配置参数 Runner.gradleRun(inputParam); } //com.tencent.tinker.build.patch.Runner public static void gradleRun(InputParam inputParam) { mBeginTime = System.currentTimeMillis(); Runner m = new Runner(); m.run(inputParam); } private void run(InputParam inputParam) { loadConfigFromGradle(inputParam); try { Logger.initLogger(config); //调用成员方法,开始差分 tinkerPatch(); } catch (IOException e) { e.printStackTrace(); goToError(); } finally { Logger.closeLogger(); } }
//com.tencent.tinker.patch.CliMainpublic class CliMain extends Runner { public static void main(String[] args) { mBeginTime = System.currentTimeMillis(); CliMain m = new CliMain(); setRunningLocation(m); m.run(args); } private void run(String[] args) { ... ReadArgs readArgs = new ReadArgs(args).invoke(); File configFile = readArgs.getConfigFile(); File outputFile = readArgs.getOutputFile(); File oldApkFile = readArgs.getOldApkFile(); File newApkFile = readArgs.getNewApkFile(); ... //读取配置到Configuration类对象中 loadConfigFromXml(configFile, outputFile, oldApkFile, newApkFile); //调用父类Runner对象的方法,开始差分 tinkerPatch(); } }
Runner对象的成员方法tinkerPatch()定义了整个差分流程,包括Apk差分(Dex差分、Library差分、Res资源差分)、保存差分信息到文件、将差分后的文件打包、签名、压缩。这三步分别交给三个类去实现:ApkDecoder、PatchInfo、PatchBuilder
//com.tencent.tinker.build.patch.Runnerprotected void tinkerPatch() { Logger.d("-----------------------Tinker patch begin-----------------------"); Logger.d(config.toString()); try { //gen patch ApkDecoder decoder = new ApkDecoder(config); decoder.onAllPatchesStart(); decoder.patch(config.mOldApkFile, config.mNewApkFile); decoder.onAllPatchesEnd(); //gen meta file and version file PatchInfo info = new PatchInfo(config); info.gen(); //build patch PatchBuilder builder = new PatchBuilder(config); builder.buildPatch(); } catch (Throwable e) { e.printStackTrace(); goToError(); } Logger.d("Tinker patch done, total time cost: %fs", diffTimeFromBegin()); Logger.d("Tinker patch done, you can go to file to find the output %s", config.mOutFolder); Logger.d("-----------------------Tinker patch end-------------------------"); }
由于PatchInfo和PatchBuilder只是做一些收尾工作,不妨碍整个Apk差分流程分析,所以就不展开代码了。但是其中有很多开发中的小知识点可以学习,比如将一个目录下的所有文件打包成一个zip文件(IO流操作)、签名(ProcessBuilder)、7zip压缩等。
ApkDecoder
ApkDecoder的构造方法:
//com.tencent.tinker.build.decoder.ApkDecoderpublic class ApkDecoder extends BaseDecoder{ ArrayList<File> resDuplicateFiles; public ApkDecoder(Configuration config) throws IOException { super(config); //新旧Apk目录 this.mNewApkDir = config.mTempUnzipNewDir; this.mOldApkDir = config.mTempUnzipOldDir; this.manifestDecoder = new ManifestDecoder(config); //将差分过程的数据信息放到assets目录下 //例如新旧和差分dex的md5值,新增和修改资源文件 String prePath = TypedValue.FILE_ASSETS + File.separator; dexPatchDecoder = new UniqueDexDiffDecoder(config, prePath + TypedValue.DEX_META_FILE, TypedValue.DEX_LOG_FILE); soPatchDecoder = new BsDiffDecoder(config, prePath + TypedValue.SO_META_FILE, TypedValue.SO_LOG_FILE); resPatchDecoder = new ResDiffDecoder(config, prePath + TypedValue.RES_META_TXT, TypedValue.RES_LOG_FILE); resDuplicateFiles = new ArrayList<>(); } }
从ApkDecoder的构造函数可以看出来,一个Apk文件的差分又分成了对manifest、dex、so、res文件的分别差分,ApkDecoder将这三部分的差分分别交由ManifestDecoder、UniqueDexDiffDecoder、BsDiffDecoder、ResDiffDecoder三个类去做。由类名称可以看出So文件的差分是使用了BsDiff算法实现的。同时构造了一个List来存储重复的文件,这个List的作用后面再详细说明。
上述各种Decoder均继承自BaseDecoder,BaseDecoder中定义了两个文件比较粗略的差分流程:onAllPatchesStart() -> patch(File oldFile, File newFile) -> onAllPatchesEnd() -> clean(),每个BaseDecoder的子类根据不同的文件类型来实现不同的差分方式,还可以在差分之前和差分之后做一些特殊的处理。例如Dex的差分是在onAllPatchEnd()步骤中实现的,而不是在patch(File oldFile, File newFile)方法中。下面会详细分析各个Decoder中三个方法的具体实现。
ApkDecoder
ApkDecoder虽然也继承自BaseDecoder,但是它不做任何实际的差分工作,而是起到对各个类型文件差分过程进行分发和统一管理的作用。下面是ApkDecoder的具体实现。
//com.tencent.tinker.build.decoder.ApkDecoderpublic class ApkDecoder extends BaseDecoder{ ... //通知各个类型的Decoder要开始差分了,可以做一些准备工作 @Override public void onAllPatchesStart() throws IOException, TinkerPatchException { manifestDecoder.onAllPatchesStart(); dexPatchDecoder.onAllPatchesStart(); soPatchDecoder.onAllPatchesStart(); resPatchDecoder.onAllPatchesStart(); } //这里的oldFile和newFile俩个参数代表新旧Apk文件 //完整的Apk文件不能直接做差分,而这个patch方法要做的就是: //1、解压新旧Apk文件 //2、已新Apk文件为基准,遍历所有文件,根据文件类型分到具体的Decoder中去做差分操作 //3、对于那些满足dex或so文件模式的资源,打印错误日志 //4、通知各个Decoder差分结束,做收尾工作并清除占用资源 public boolean patch(File oldFile, File newFile) throws Exception { writeToLogFile(oldFile, newFile); //check manifest change first manifestDecoder.patch(oldFile, newFile); unzipApkFiles(oldFile, newFile); Files.walkFileTree(mNewApkDir.toPath(), new ApkFilesVisitor(config, mNewApkDir.toPath(), mOldApkDir.toPath(), dexPatchDecoder, soPatchDecoder, resPatchDecoder)); //get all duplicate resource file for (File duplicateRes : resDuplicateFiles) { //resPatchDecoder.patch(duplicateRes, null); Logger.e("Warning: res file %s is also match at dex or library pattern, " + "we treat it as unchanged in the new resource_out.zip", getRelativePathStringToOldFile(duplicateRes)); } soPatchDecoder.onAllPatchesEnd(); dexPatchDecoder.onAllPatchesEnd(); manifestDecoder.onAllPatchesEnd(); resPatchDecoder.onAllPatchesEnd(); //clean resources dexPatchDecoder.clean(); soPatchDecoder.clean(); resPatchDecoder.clean(); return true; } @Override public void onAllPatchesEnd() throws IOException, TinkerPatchException { } ...}
ManifestDecoder
ManifestDecoder的onAllPatchesStart()方法和onAllPatchesEnd()方法都是空实现,所以只需要关注patch方法即可。
//com.tencent.tinker.build.decoder.ManifestDecoderpublic class ManifestDecoder extends BaseDecoder { ... @Override public boolean patch(File oldFile, File newFile) throws IOException, TinkerPatchException { final boolean ignoreWarning = config.mIgnoreWarning; try { //解析新旧AndroidManifest.xml文件,方便属性的读取 //使用的是org.w3c.dom.Document解析的xml文件 AndroidParser oldAndroidManifest = AndroidParser.getAndroidManifest(oldFile); AndroidParser newAndroidManifest = AndroidParser.getAndroidManifest(newFile); //检查支持的最低sdk版本,如果低于14,必须设置dexMode为jar模式 int minSdkVersion = Integer.parseInt(oldAndroidManifest.apkMeta.getMinSdkVersion()); if (minSdkVersion < TypedValue.ANDROID_40_API_LEVEL) { if (config.mDexRaw) { if (ignoreWarning) { //ignoreWarning, just log Logger.e("Warning:ignoreWarning is true, but your old apk's minSdkVersion %d is below 14, you should set the dexMode to 'jar', otherwise, it will crash at some time", minSdkVersion); } else { Logger.e("Warning:ignoreWarning is false, but your old apk's minSdkVersion %d is below 14, you should set the dexMode to 'jar', otherwise, it will crash at some time", minSdkVersion); throw new TinkerPatchException( String.format("ignoreWarning is false, but your old apk's minSdkVersion %d is below 14, you should set the dexMode to 'jar', otherwise, it will crash at some time", minSdkVersion) ); } } } //双层for循环,检查是否有新增组件,Tinker不支持新增组件。 List<String> oldAndroidComponent = oldAndroidManifest.getComponents(); List<String> newAndroidComponent = newAndroidManifest.getComponents(); for (String newComponentName : newAndroidComponent) { boolean found = false; for (String oldComponentName : oldAndroidComponent) { if (newComponentName.equals(oldComponentName)) { found = true; break; } } if (!found) { if (ignoreWarning) { Logger.e("Warning:ignoreWarning is true, but we found a new AndroidComponent %s, it will crash at some time", newComponentName); } else { Logger.e("Warning:ignoreWarning is false, but we found a new AndroidComponent %s, it will crash at some time", newComponentName); throw new TinkerPatchException( String.format("ignoreWarning is false, but we found a new AndroidComponent %s, it will crash at some time", newComponentName) ); } } } } catch (ParseException e) { e.printStackTrace(); throw new TinkerPatchException("parse android manifest error!"); } return false; } ...}
经过上述对ManifestDecoder的逻辑分析,发现并没有对新旧AndroidManifest.xml文件做差分,原因就是Tinker不支持新增四大组件。后面对补丁包合并的过程分析可以知道,在客户端合成的全量Apk中使用的是旧Apk中的AndroidManifest.xml文件。
BsDiffDecoder
BsDiffDecoder中的onAllPatchesStart()和onAllPatchesEnd()方法同样是空实现,还是主要分析patch方法。
com.tencent.tinker.build.decoder.BsDiffDecoderpublic class BsDiffDecoder extends BaseDecoder { @Override public boolean patch(File oldFile, File newFile) throws IOException, TinkerPatchException { //参数合法性检查 if (newFile == null || !newFile.exists()) { return false; } //获取新文件的md5值,新建差分文件 String newMd5 = MD5.getMD5(newFile); File bsDiffFile = getOutputPath(newFile).toFile(); //如果对应的旧文件不存在,说明是新增文件,直接将新文件拷贝到差分文件中 if (oldFile == null || !oldFile.exists()) { FileOperation.copyFileUsingStream(newFile, bsDiffFile); writeLogFiles(newFile, null, null, newMd5); return true; } //空文件不做差分 if (oldFile.length() == 0 && newFile.length() == 0) { return false; } //如果有一个文件是空的,直接将新文件作为差分文件 if (oldFile.length() == 0 || newFile.length() == 0) { FileOperation.copyFileUsingStream(newFile, bsDiffFile); writeLogFiles(newFile, null, null, newMd5); return true; } //获取旧文件的MD5 String oldMd5 = MD5.getMD5(oldFile); //新旧MD5相同,则文件未更改,不做差分 if (oldMd5.equals(newMd5)) { return false; } if (!bsDiffFile.getParentFile().exists()) { bsDiffFile.getParentFile().mkdirs(); } //调用bsdiff方法做差分 BSDiff.bsdiff(oldFile, newFile, bsDiffFile); //检查差分文件大小,如果大于新文件的80%,则按照新增文件处理 if (Utils.checkBsDiffFileSize(bsDiffFile, newFile)) { writeLogFiles(newFile, oldFile, bsDiffFile, newMd5); } else { FileOperation.copyFileUsingStream(newFile, bsDiffFile); writeLogFiles(newFile, null, null, newMd5); } return true; }}
ResDiffDecoder
由于ResDiffDecoder和BsDiffDecoder有相似之处,所以在分析完ResDiffDecoder的逻辑之后再总结资源文件和So文件差分思想。
com.tencent.tinker.build.decoder.ResDiffDecoder //构造函数 public ResDiffDecoder(Configuration config, String metaPath, String logPath) throws IOException { super(config); ... //新增文件集合 addedSet = new ArrayList<>(); //修改文件集合 modifiedSet = new ArrayList<>(); //大文件修改集合 largeModifiedSet = new ArrayList<>(); //修改的大文件信息 largeModifiedMap = new HashMap<>(); //删除文件集合 deletedSet = new ArrayList<>(); } @Override public boolean patch(File oldFile, File newFile) throws IOException, TinkerPatchException { String name = getRelativePathStringToNewFile(newFile); //新文件不存在,检查是否忽略此文件,按照删除文件类型处理 //实际上是不会出现这种情况,因为是按照新Apk为基准做差分的 if (newFile == null || !newFile.exists()) { String relativeStringByOldDir = getRelativePathStringToOldFile(oldFile); if (Utils.checkFileInPattern(config.mResIgnoreChangePattern, relativeStringByOldDir)) { Logger.e("found delete resource: " + relativeStringByOldDir + " ,but it match ignore change pattern, just ignore!"); return false; } deletedSet.add(relativeStringByOldDir); writeResLog(newFile, oldFile, TypedValue.DEL); return true; } File outputFile = getOutputPath(newFile).toFile(); //如果旧文件不存在,则说明是新增的文件 if (oldFile == null || !oldFile.exists()) { if (Utils.checkFileInPattern(config.mResIgnoreChangePattern, name)) { Logger.e("found add resource: " + name + " ,but it match ignore change pattern, just ignore!"); return false; } FileOperation.copyFileUsingStream(newFile, outputFile); addedSet.add(name); writeResLog(newFile, oldFile, TypedValue.ADD); return true; } //空文件不做差分 if (oldFile.length() == 0 && newFile.length() == 0) { return false; } //如果两个文件都不是空文件,应该是文件变更,先获取文件的MD5 String newMd5 = MD5.getMD5(newFile); String oldMd5 = MD5.getMD5(oldFile); //如果MD5值相同,则文件未发生变化 if (oldMd5 != null && oldMd5.equals(newMd5)) { return false; } //检查是否忽略此文件 if (Utils.checkFileInPattern(config.mResIgnoreChangePattern, name)) { Logger.d("found modify resource: " + name + ", but it match ignore change pattern, just ignore!"); return false; } //AndroidManifest.xml不做差分 if (name.equals(TypedValue.RES_MANIFEST)) { Logger.d("found modify resource: " + name + ", but it is AndroidManifest.xml, just ignore!"); return false; } //判断resources.arsc文件是否变化 if (name.equals(TypedValue.RES_ARSC)) { if (AndroidParser.resourceTableLogicalChange(config)) { Logger.d("found modify resource: " + name + ", but it is logically the same as original new resources.arsc, just ignore!"); return false; } } //处理变化的文件 dealWithModeFile(name, newMd5, oldFile, newFile, outputFile); return true; } //处理变更的文件 private boolean dealWithModeFile(String name, String newMd5, File oldFile, File newFile, File outputFile) throws IOException { //判断是否是大文件变更,可配置,默认是100KB //如果文件超出100KB才做文件差分,否则按照新增文件处理。 //并且差分文件不得大于新文件的80%,否则也按照新增文件处理 if (checkLargeModFile(newFile)) { if (!outputFile.getParentFile().exists()) { outputFile.getParentFile().mkdirs(); } BSDiff.bsdiff(oldFile, newFile, outputFile); //treat it as normal modify if (Utils.checkBsDiffFileSize(outputFile, newFile)) { LargeModeInfo largeModeInfo = new LargeModeInfo(); largeModeInfo.path = newFile; largeModeInfo.crc = FileOperation.getFileCrc32(newFile); largeModeInfo.md5 = newMd5; largeModifiedSet.add(name); largeModifiedMap.put(name, largeModeInfo); writeResLog(newFile, oldFile, TypedValue.LARGE_MOD); return true; } } modifiedSet.add(name); FileOperation.copyFileUsingStream(newFile, outputFile); writeResLog(newFile, oldFile, TypedValue.MOD); return false; }
到此已经将所有的资源文件都按照新增、修改的变更方式做了处理,并存储了文件名称和变更的文件信息,这里并没有处理删除的资源文件,因为获取删除资源集合是在onAllPatchEnd()方法中处理的。接下来就是在onAllPatchesEnd()方法中将所有变更和添加的文件压缩到resources_out.zip文件,并将新增、删除、修改的文件信息写到res_meta.txt文件中。
res_meta.txt文件示例如下
resources_out.zip,1134154093,26a3339220be96e865fc523fe4a162a8pattern:3resources.arscres/*assets/*large modify:1resources.arsc,26a3339220be96e865fc523fe4a162a8,946796371modify:1res/layout/activity_bug.xmladd:199res/drawable-hdpi-v4/abc_ab_share_pack_mtrl_alpha.9.pngres/drawable-hdpi-v4/abc_btn_check_to_on_mtrl_000.pngres/drawable-hdpi-v4/abc_btn_check_to_on_mtrl_015.pngres/drawable-hdpi-v4/abc_btn_radio_to_on_mtrl_000.pngres/drawable-hdpi-v4/abc_btn_radio_to_on_mtrl_015.png
BsDiffDecoder和ResDiffDecoder差分思想
在分析两者的源码时,均发现在做完差分后,有对差分文件检查大小的步骤。如果差分文件超过新文件的80%,则放弃使用差分文件,直接使用新文件,即按照新增文件或变更文件处理。
com.tencent.tinker.build.util public static boolean checkBsDiffFileSize(File bsDiffFile, File newFile) { ··· //计算差分文件相对于新文件的大小 double ratio = bsDiffFile.length() / (double) newFile.length(); //如果这个比例大于预设的值0.8返回false。 if (ratio > TypedValue.BSDIFF_PATCH_MAX_RATIO) { Logger.e("bsDiff patch file:%s, size:%dk, new file:%s, size:%dk. patch file is too large, treat it as newly file to save patch time!", bsDiffFile.getName(), bsDiffFile.length() / 1024, newFile.getName(), newFile.length() / 1024 ); return false; } return true; }
BsDiff 算法
快速后缀排序
DexDiffDecoder
检查在新的Dex中,那些已经被排除的类是否被更改。主要是检查Tinker相关的类,因为Loader类只会出现在主Dex中,并且新旧Dex中的Loader类应当保持一致。
当出现下面情况时,会声明异常:
- 不存在新主Dex
- 不存在旧主Dex
- 旧主Dex中不存在Loader类
- 新主Dex出现新的Loader类
- 旧主Dex中的Loader类出现变化或被删除
- Loader类出现在新的二级Dex文件中
- Loader类出现在旧的二级Dex文件中
前置条件检查完成后,将Dex差分分为两种情况:
- 新增Dex(新Dex文件存在,旧Dex文件不存在)
- Dex类变化(新旧Dex文件都存在,但MD5值不同)
情况1很好处理,直接将新Dex文件拷贝到临时目录。
情况2就要对两个Dex文件做分析,找出变化的类。Tinker在对Dex做差分时,分成了两步。第一步在patch(final File oldFile, final File newFile)方法中先把新增和删除的类集合找出来。第二步在onAllPatchesEnd()方法中真正去做差分并生成差分Dex文件。
com.tencent.tinker.build.decoder.DexDiffDecoder @Override public void onAllPatchesEnd() throws Exception { //先判断是否有Dex文件变更 if (!hasDexChanged) { Logger.d("No dexes were changed, nothing needs to be done next."); return; } //判断是否启用加固 if (config.mIsProtectedApp) { generateChangedClassesDexFile(); } else { generatePatchInfoFile(); } }
处理Dex文件差分的类叫做DexPatchGernerator。
com.tencent.tinker.build.dexpatcher.DexPatchGernerator//构造函数public DexPatchGenerator(Dex oldDex, Dex newDex) { this.oldDex = oldDex; this.newDex = newDex; //新旧Dex、新Dex和差分Dex、旧Dex和差分Dex两两Dex中各个块的索引对应 SparseIndexMap oldToNewIndexMap = new SparseIndexMap(); SparseIndexMap oldToPatchedIndexMap = new SparseIndexMap(); SparseIndexMap newToPatchedIndexMap = new SparseIndexMap(); SparseIndexMap selfIndexMapForSkip = new SparseIndexMap(); //需要额外移除的类正则式集合 additionalRemovingClassPatternSet = new HashSet<>(); //根据Dex文件不同的块,构造对应的差分算法进行差分操作。例如字符串块的差分算法StringDataSectionDiffAlgorithm this.stringDataSectionDiffAlg = new StringDataSectionDiffAlgorithm( oldDex, newDex, oldToNewIndexMap, oldToPatchedIndexMap, newToPatchedIndexMap, selfIndexMapForSkip ); //其他部分差分算法 ...
接下来就要执行各个块的差分算法了。
com.tencent.tinker.build.dexpatcher.DexPatchGerneratorpublic void executeAndSaveTo(OutputStream out) throws IOException { //第一步,在新Dex收集需要移除的块信息,并将它们设置到对应的差分算法实现中。一般都是需要移除一部分不需要做差分的类。 ... List<Integer> typeIdOfClassDefsToRemove = new ArrayList<>(classNamePatternCount); List<Integer> offsetOfClassDatasToRemove = new ArrayList<>(classNamePatternCount); //双层for循环,找出移除类的索引和数据块偏移 for (ClassDef classDef : this.newDex.classDefs()) { String typeName = this.newDex.typeNames().get(classDef.typeIndex); for (Pattern pattern : classNamePatterns) { if (pattern.matcher(typeName).matches()) { typeIdOfClassDefsToRemove.add(classDef.typeIndex); offsetOfClassDatasToRemove.add(classDef.classDataOffset); break; } } } ... //第二步,执行各个块的差分算法 //1. 执行差分算法,计算需要添加、删除、替换的item索引 //2. 执行补丁模拟算法,计算item索引和偏移量的对应关系。立刻执行补丁模拟算法可以知道新旧Dex文件相对于差分Dex文件item索引和偏移量的对应关系,这些信息在后面差分工作中非常重要。 }
以StringDataSection为例,说明具体差分步骤:
- 读取旧Dex文件中的StringData块,存储形式为AbstractMap.SimpleEntry
private void writeResultToStream(OutputStream os) throws IOException { DexDataBuffer buffer = new DexDataBuffer(); //写入魔数,代表是Dex差分文件,固定为DXDIFF buffer.write(DexPatchFile.MAGIC); //写入Patch文件格式版本 buffer.writeShort(DexPatchFile.CURRENT_VERSION); //写入文件大小 buffer.writeInt(this.patchedDexSize); // 这里还不知道第一个数据块的偏移,所以等其他偏移量都写入后再回来写。先写一个0占位 int posOfFirstChunkOffsetField = buffer.position(); buffer.writeInt(0); //写入各个map list相关的偏移量,这些偏移量应该是新Dex中的偏移,目的是做校验 buffer.writeInt(this.patchedStringIdsOffset); buffer.writeInt(this.patchedTypeIdsOffset); buffer.writeInt(this.patchedProtoIdsOffset); buffer.writeInt(this.patchedFieldIdsOffset); buffer.writeInt(this.patchedMethodIdsOffset); buffer.writeInt(this.patchedClassDefsOffset); buffer.writeInt(this.patchedMapListOffset); buffer.writeInt(this.patchedTypeListsOffset); buffer.writeInt(this.patchedAnnotationSetRefListItemsOffset); buffer.writeInt(this.patchedAnnotationSetItemsOffset); buffer.writeInt(this.patchedClassDataItemsOffset); buffer.writeInt(this.patchedCodeItemsOffset); buffer.writeInt(this.patchedStringDataItemsOffset); buffer.writeInt(this.patchedDebugInfoItemsOffset); buffer.writeInt(this.patchedAnnotationItemsOffset); buffer.writeInt(this.patchedEncodedArrayItemsOffset); buffer.writeInt(this.patchedAnnotationsDirectoryItemsOffset); //写入文件签名,计算签名时除去文件开头的32字节 buffer.write(this.oldDex.computeSignature(false)); int firstChunkOffset = buffer.position(); //回到posOfFirstChunkOffsetField位置 buffer.position(posOfFirstChunkOffsetField); //写入第一个数据块的偏移 buffer.writeInt(firstChunkOffset); //将buffer写入点移到firstChunkOffset buffer.position(firstChunkOffset); //写入各个算法计算出来的差分信息 //1. 先写入删除、添加、替换的Item的个数 //2. 然后按照删除、添加、替换的顺序,写入每一个Item索引与上一个Item索引的差值? //3. 写入需要添加和替换的Item数据 writePatchOperations(buffer, this.stringDataSectionDiffAlg.getPatchOperationList()); writePatchOperations(buffer, this.typeIdSectionDiffAlg.getPatchOperationList()); writePatchOperations(buffer, this.typeListSectionDiffAlg.getPatchOperationList()); writePatchOperations(buffer, this.protoIdSectionDiffAlg.getPatchOperationList()); writePatchOperations(buffer, this.fieldIdSectionDiffAlg.getPatchOperationList()); writePatchOperations(buffer, this.methodIdSectionDiffAlg.getPatchOperationList()); writePatchOperations(buffer, this.annotationSectionDiffAlg.getPatchOperationList()); writePatchOperations(buffer, this.annotationSetSectionDiffAlg.getPatchOperationList()); writePatchOperations(buffer, this.annotationSetRefListSectionDiffAlg.getPatchOperationList()); writePatchOperations(buffer, this.annotationsDirectorySectionDiffAlg.getPatchOperationList()); writePatchOperations(buffer, this.debugInfoSectionDiffAlg.getPatchOperationList()); writePatchOperations(buffer, this.codeSectionDiffAlg.getPatchOperationList()); writePatchOperations(buffer, this.classDataSectionDiffAlg.getPatchOperationList()); writePatchOperations(buffer, this.encodedArraySectionDiffAlg.getPatchOperationList()); writePatchOperations(buffer, this.classDefSectionDiffAlg.getPatchOperationList()); byte[] bufferData = buffer.array(); //写入文件 os.write(bufferData); os.flush(); }
至此,一个Apk文件中的所有部分均已差分完成,并写入到临时文件中了。剩下就是做一些清理工作,然后生成补丁补充信息文件如TinkerId、HotPatch版本等,最后是将所有文件打包成Apk包、签名、压缩。
APK Patch Recovery 补丁合成
- Tinker默认实现了DefaultPatchListener,开发者可自定义收到新补丁的操作,实现PatchListener,并设置给Tinker。
- DefaultPatchListener会启动:patch进程(TinkerPatchService)来执行补丁合并操作。
- 在TinkerPatchService的onHandleIntent方法中会使用默认的补丁处理器(UpgradePatch),当然开发者也可以自己实现,在Tinker初始化时调用install方法。补丁处理完成后的操作是由一个IntentService处理的,包括删除原始补丁文件,杀死:patch进程,重启主进程等。
- 为了防止补丁合成进程被系统杀死,特意提高了:patch进程的优先级。
Tinker的默认补丁升级操作实现类为UpgradePatch(com.tencent.tinker.lib.patch),主要包含了以下几个步骤:补丁验证 -> 准备 -> 合并dex -> 合并so文件 -> 合并res文件 -> 等待并验证opt文件(部分机型) -> 将补丁信息写回path.info文件中 -> 补丁升级结束
//com.tencent.tinker.lib.patch.AbstractPatchpublic abstract class AbstractPatch { //三个参数 //context 上下文 //tempPatchPath 补丁包路径 //patchResult 补丁合成结果 public abstract boolean tryPatch(Context context, String tempPatchPath, PatchResult patchResult);}
补丁验证
验证签名
获取证书公钥
ByteArrayInputStream stream = null;PackageManager pm = context.getPackageManager();String packageName = context.getPackageName();PackageInfo packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES);CertificateFactory certFactory = CertificateFactory.getInstance("X.509");stream = new ByteArrayInputStream(packageInfo.signatures[0].toByteArray());X509Certificate cert = (X509Certificate) certFactory.generateCertificate(stream);mPublicKey = cert.getPublicKey();
验证补丁jar包中的各个部分
//com.tencent.tinker.loader.shareutil.ShareSecurityCheckpublic boolean verifyPatchMetaSignature(File path) { ... JarFile jarFile = null; try { //以JarFile形式读取补丁包 jarFile = new JarFile(path); //得到压缩包中的条目 final Enumeration<JarEntry> entries = jarFile.entries(); //遍历 while (entries.hasMoreElements()) { JarEntry jarEntry = entries.nextElement(); // no code if (jarEntry == null) { continue; } //META-INF目录下的文件不验证 final String name = jarEntry.getName(); if (name.startsWith("META-INF/")) { continue; } //为了提高速度,只验证.meta结尾的文件,其他条目会以写在meta文件中的MD5值做校验 if (!name.endsWith(ShareConstants.META_SUFFIX)) { continue; } //存储meta文件内容到内存中,为了快 metaContentMap.put(name, SharePatchFileUtil.loadDigestes(jarFile, jarEntry)); //获取条目的签名证书 Certificate[] certs = jarEntry.getCertificates(); if (certs == null) { return false; } //验证条目的证书公钥是否一致 if (!check(path, certs)) { return false; } } } catch (Exception e) { throw new TinkerRuntimeException( String.format("ShareSecurityCheck file %s, size %d verifyPatchMetaSignature fail", path.getAbsolutePath(), path.length()), e); } finally { try { if (jarFile != null) { jarFile.close(); } } catch (IOException e) { Log.e(TAG, path.getAbsolutePath(), e); } } return true; } private boolean check(File path, Certificate[] certs) { if (certs.length > 0) { for (int i = certs.length - 1; i >= 0; i--) { try { //验证所有证书的公钥,一般只有一个 certs[i].verify(mPublicKey); return true; } catch (Exception e) { Log.e(TAG, path.getAbsolutePath(), e); } } } return false; }
验证是否启用Tinker及是否支持dex、lib、res模式
准备
检查补丁信息
补丁信息文件patch.info记录了新旧补丁版本、系统指纹、OAT目录。
在读取patch.info文件时,用到了文件锁定,锁定文件为info.lock。每次需要读取或写入patch.info文件时,需要获取info.lock文件的锁定,如果获取不到说明有其他线程正在操作patch.info文件。
//com.tencent.tinker.loader.shareutil.ShareFileLockHelper //获取锁定次数 public static final int MAX_LOCK_ATTEMPTS = 3; //每次获取锁定等待时间 public static final int LOCK_WAIT_EACH_TIME = 10;private ShareFileLockHelper(File lockFile) throws IOException { outputStream = new FileOutputStream(lockFile); int numAttempts = 0; boolean isGetLockSuccess; FileLock localFileLock = null; //just wait twice, Exception saveException = null; while (numAttempts < MAX_LOCK_ATTEMPTS) { numAttempts++; try { localFileLock = outputStream.getChannel().lock(); isGetLockSuccess = (localFileLock != null); if (isGetLockSuccess) { break; } //it can just sleep 0, afraid of cpu scheduling Thread.sleep(LOCK_WAIT_EACH_TIME); } catch (Exception e) { saveException = e; Log.e(TAG, "getInfoLock Thread failed time:" + LOCK_WAIT_EACH_TIME); } } if (localFileLock == null) { throw new IOException("Tinker Exception:FileLockHelper lock file failed: " + lockFile.getAbsolutePath(), saveException); } fileLock = localFileLock; }
将补丁拷贝到应用私有目录
//com.tencent.tinker.loader.shareutil.SharePatchFileUtil public static void copyFileUsingStream(File source, File dest) throws IOException { //检查文件合法性 if (!SharePatchFileUtil.isLegalFile(source) || dest == null) { return; } if (source.getAbsolutePath().equals(dest.getAbsolutePath())) { return; } FileInputStream is = null; FileOutputStream os = null; //创建目的文件目录 File parent = dest.getParentFile(); if (parent != null && (!parent.exists())) { parent.mkdirs(); } try { //输入输出流互写 is = new FileInputStream(source); os = new FileOutputStream(dest, false); byte[] buffer = new byte[ShareConstants.BUFFER_SIZE]; int length; while ((length = is.read(buffer)) > 0) { os.write(buffer, 0, length); } } finally { closeQuietly(is); closeQuietly(os); } }
合并
Dex文件合并
从补丁文件中提取出Dex差分文件
从dex_meta.txt文件中解析出每一个dex差分文件的信息,存储到ShareDexDiffPatchInfo列表中。包含以下字段:
// com.tencent.tinker.loader.shareutil.ShareDexDiffPatchInfopublic final String rawName; //dex原始文件名称,例如classes2.dexpublic final String destMd5InDvm; //在Dvm虚拟机中合成后的Dex的md5值public final String destMd5InArt; //在Art虚拟机中合成后的Dex的md5值public final String oldDexCrC; //旧Dex的crc值public final String dexDiffMd5; //差分Dex文件的MD5值public final String path; //差分Dex相对于合成后的Dex文件父目录public final String dexMode; //dex压缩方式raw或jarpublic final boolean isJarMode; //dex压缩方式是否是jar模式public final String realName; //差分dex文件的真实文件名称,如果是jar模式,需要在rawName的基础上添加.jar后缀
从差分包或原始Apk中提取出dex文件到packagename/tinker/patch-basd22fa/dex目录中
- 新增Dex,从差分包提取dex,放到dex目录下
- 变化特别大dex,直接从原始apk中提取dex,放到dex目录下
- 差分dex与原始dex合成后,放到dex目录下
dex补丁合成
如果补丁中的dex文件使用了jar模式,还需要再使用ZipInputStream流包装一次
之后交给DexPatchApplier执行dex文件合并,并存储到dex目录下
// com.tencent.tinker.commons.dexpatcher.DexPatchApplier//构造函数public DexPatchApplier(Dex oldDexIn, DexPatchFile patchFileIn) { this.oldDex = oldDexIn; //旧dex this.patchFile = patchFileIn;//补丁dex this.patchedDex = new Dex(patchFileIn.getPatchedDexSize());//合成后的dex this.oldToPatchedIndexMap = new SparseIndexMap();}//验证两个dex的签名是否匹配byte[] oldDexSign = this.oldDex.computeSignature(false);byte[] oldDexSignInPatchFile = this.patchFile.getOldDexSignature();if (CompareUtils.uArrCompare(oldDexSign, oldDexSignInPatchFile) != 0) { throw new IOException( String.format( "old dex signature mismatch! expected: %s, actual: %s", Arrays.toString(oldDexSign), Arrays.toString(oldDexSignInPatchFile) ) ); }//1、先将TableOfContents的偏移写入合成后的dex文件中,然后就可以计算出TableOfContents的大小//2、根据各个数据块的依赖关系执行各个数据块的合并算法//3、 写入文件头,mapList。计算并写入合成后的dex文件签名和校验和Dex.Section headerOut = this.patchedDex.openSection(patchedToc.header.off);patchedToc.writeHeader(headerOut);Dex.Section mapListOut=this.patchedDex.openSection(patchedToc.mapList.off);patchedToc.writeMap(mapListOut);this.patchedDex.writeHashes();//4、 将合并后的dex写入文件中。this.patchedDex.writeTo(out);
下面以StringDataSection为例看一下合并过程:
//com.tencent.tinker.commons.dexpatcher.algorithms.patch.DexSectionPatchAlgorithmprivate void doFullPatch( Dex.Section oldSection, //旧StringSection int oldItemCount, //旧StringSection中数据项个数 int[] deletedIndices, //删除项的索引或偏移量数组 int[] addedIndices, //新增项的索引或偏移量数组 int[] replacedIndices //替换项的索引或偏移量数组) { int deletedItemCount = deletedIndices.length; int addedItemCount = addedIndices.length; int replacedItemCount = replacedIndices.length; int newItemCount = oldItemCount + addedItemCount - deletedItemCount; int deletedItemCounter = 0; int addActionCursor = 0; int replaceActionCursor = 0; int oldIndex = 0; int patchedIndex = 0; //核心合并算法 while (oldIndex < oldItemCount || patchedIndex < newItemCount) { //写入新增项 if (addActionCursor < addedItemCount && addedIndices[addActionCursor] == patchedIndex) { T addedItem = nextItem(patchFile.getBuffer()); int patchedOffset = writePatchedItem(addedItem); ++addActionCursor; ++patchedIndex; } else if (replaceActionCursor < replacedItemCount && replacedIndices[replaceActionCursor] == patchedIndex) { //写入补丁中的变更项 T replacedItem = nextItem(patchFile.getBuffer()); int patchedOffset = writePatchedItem(replacedItem); ++replaceActionCursor; ++patchedIndex; } else if (Arrays.binarySearch(deletedIndices, oldIndex) >= 0) { //忽略删除项 T skippedOldItem = nextItem(oldSection); // skip old item. markDeletedIndexOrOffset( oldToPatchedIndexMap, oldIndex, getItemOffsetOrIndex(oldIndex, skippedOldItem) ); ++oldIndex; ++deletedItemCounter; } else if (Arrays.binarySearch(replacedIndices, oldIndex) >= 0) { //忽略旧StringSection中的变更项 T skippedOldItem = nextItem(oldSection); // skip old item. markDeletedIndexOrOffset( oldToPatchedIndexMap, oldIndex, getItemOffsetOrIndex(oldIndex, skippedOldItem) ); ++oldIndex; } else if (oldIndex < oldItemCount) { //写入未变更项 T oldItem = adjustItem(this.oldToPatchedIndexMap, nextItem(oldSection)); int patchedOffset = writePatchedItem(oldItem); updateIndexOrOffset( this.oldToPatchedIndexMap, oldIndex, getItemOffsetOrIndex(oldIndex, oldItem), patchedIndex, patchedOffset ); ++oldIndex; ++patchedIndex; } }}
odex
- 优化后的dex存储目录为odex。
- 在art虚拟机下,opt操作是并行的
- 在dalvik虚拟机下,机器硬件性能比较低下,串行opt操作
//com.tencent.tinker.lib.patch.DexDiffPatchInternalprivate static boolean patchDexExtractViaDexDiff(Context context, String patchVersionDirectory, String meta, final File patchFile) { ...final Tinker manager = Tinker.with(context);File dexFiles = new File(dir);//需要优化的dex文件数组File[] files = dexFiles.listFiles();//清空旧的opt文件optFiles.clear();if (files != null) { //opt目录路径 patch-asd42fa/odex/ final String optimizeDexDirectory = patchVersionDirectory + "/" + DEX_OPTIMIZE_PATH + "/"; //odex目录 File optimizeDexDirectoryFile = new File(optimizeDexDirectory); // 新建odex文件,例:dex/classes.dex -> odex/classes.dex for (File file : files) { String outputPathName = SharePatchFileUtil.optimizedPathFor(file, optimizeDexDirectoryFile); optFiles.add(new File(outputPathName)); } // Art虚拟机并行dexopt或dex2oat操作 默认2个线程 if (ShareTinkerInternals.isVmArt()) { //失败的opt文件集合 final List<File> failOptDexFile = new Vector<>(); final Throwable[] throwable = new Throwable[1]; // TinkerParallelDexOptimizer dex优化类 TinkerParallelDexOptimizer.optimizeAll( Arrays.asList(files), optimizeDexDirectoryFile, new TinkerParallelDexOptimizer.ResultCallback() { long startTime; @Override public void onStart(File dexFile, File optimizedDir) { } @Override public void onSuccess(File dexFile, File optimizedDir, File optimizedFile) { } @Override public void onFailed(File dexFile, File optimizedDir, Throwable thr) { //失败后,存储失败的文件和异常 failOptDexFile.add(dexFile); throwable[0] = thr; } } ); //如果存在失败的文件,通过PatchReporter通知给用户 if (!failOptDexFile.isEmpty()) { manager.getPatchReporter().onPatchDexOptFail(patchFile, failOptDexFile, throwable[0]); return false; } // dalvik虚拟机,串行操作 } else { for (File file : files) { try { String outputPathName = SharePatchFileUtil.optimizedPathFor(file, optimizeDexDirectoryFile); //加载dex文件并优化 DexFile.loadDex(file.getAbsolutePath(), outputPathName, 0); } catch (Throwable e) { ... return false; } } }}
Tinker在实现上并没有在合成阶段使用dex2oat,而是在加载dex的时候。
//com.tencent.tinker.loader.TinkerParallelDexOptimizerpublic static boolean optimizeAll(Collection<File> dexFiles, File optimizedDir, ResultCallback cb) { return optimizeAll(dexFiles, optimizedDir, **false**, null, cb);}//此处useInterpretMode参数一直为falsepublic static boolean optimizeAll(Collection<File> dexFiles, File optimizedDir, boolean useInterpretMode, String targetISA, ResultCallback cb) { final AtomicInteger successCount = new AtomicInteger(0); return optimizeAllLocked(dexFiles, optimizedDir, useInterpretMode, targetISA, successCount, cb, DEFAULT_THREAD_COUNT); }
Dex2Oat实现:
//com.tencent.tinker.loader.TinkerParallelDexOptimizer.OptimizeWorkerprivate void interpretDex2Oat(String dexFilePath, String oatFilePath) throws IOException { final File oatFile = new File(oatFilePath); if (!oatFile.exists()) { oatFile.getParentFile().mkdirs(); } //调用dex2oat命令 final List<String> commandAndParams = new ArrayList<>(); commandAndParams.add("dex2oat"); //dex文件路径 commandAndParams.add("--dex-file=" + dexFilePath); //oat文件目录 commandAndParams.add("--oat-file=" + oatFilePath); //指定cpu指令集(六种 arm mips x86 32|64位)编译dex文件 此处tartetISA为null commandAndParams.add("--instruction-set=" + targetISA); //指定编译选项(verify-none|speed|interpret-only|verify-at-runtime|space|balanced|everything|time)仅编译 commandAndParams.add("--compiler-filter=interpret-only"); final ProcessBuilder pb = new ProcessBuilder(commandAndParams); pb.redirectErrorStream(true); final Process dex2oatProcess = pb.start(); StreamConsumer.consumeInputStream(dex2oatProcess.getInputStream()); StreamConsumer.consumeInputStream(dex2oatProcess.getErrorStream()); try { final int ret = dex2oatProcess.waitFor(); if (ret != 0) { throw new IOException("dex2oat works unsuccessfully, exit code: " + ret); } } catch (InterruptedException e) { throw new IOException("dex2oat is interrupted, msg: " + e.getMessage(), e); } }
So文件合并
So文件的合并过程和dex文件的合并是一样的,从asset/so_meta.txt文件中读取需要合并的记录,从补丁包或原始APK中提取相应的so文件进程合并,合并后文件的存放目录为lib。
和生成补丁过程类似,so文件的合并也是利用了BsPatch算法。
//com.tencent.tinker.bsdiff.BSPatch/*** 这个补丁算法是快速的,但是需要更多的内存* 占用内存大小 = 旧文件大小 + 差分文件大小 + 新文件大小*/public static byte[] patchFast(byte[] oldBuf, int oldsize, byte[] diffBuf, int diffSize, int extLen) throws IOException { DataInputStream diffIn = new DataInputStream(new ByteArrayInputStream(diffBuf, 0, diffSize)); diffIn.skip(8); // skip headerMagic at header offset 0 (length 8 bytes) long ctrlBlockLen = diffIn.readLong(); // ctrlBlockLen after bzip2 compression at heater offset 8 (length 8 bytes) long diffBlockLen = diffIn.readLong(); // diffBlockLen after bzip2 compression at header offset 16 (length 8 bytes) int newsize = (int) diffIn.readLong(); // size of new file at header offset 24 (length 8 bytes) diffIn.close(); InputStream in = new ByteArrayInputStream(diffBuf, 0, diffSize); in.skip(BSUtil.HEADER_SIZE); DataInputStream ctrlBlockIn = new DataInputStream(new GZIPInputStream(in)); in = new ByteArrayInputStream(diffBuf, 0, diffSize); in.skip(ctrlBlockLen + BSUtil.HEADER_SIZE); InputStream diffBlockIn = new GZIPInputStream(in); in = new ByteArrayInputStream(diffBuf, 0, diffSize); in.skip(diffBlockLen + ctrlBlockLen + BSUtil.HEADER_SIZE); InputStream extraBlockIn = new GZIPInputStream(in); // byte[] newBuf = new byte[newsize + 1]; byte[] newBuf = new byte[newsize]; int oldpos = 0; int newpos = 0; int[] ctrl = new int[3]; // int nbytes; while (newpos < newsize) { for (int i = 0; i <= 2; i++) { ctrl[i] = ctrlBlockIn.readInt(); } if (newpos + ctrl[0] > newsize) { throw new IOException("Corrupt by wrong patch file."); } // Read ctrl[0] bytes from diffBlock stream if (!BSUtil.readFromStream(diffBlockIn, newBuf, newpos, ctrl[0])) { throw new IOException("Corrupt by wrong patch file."); } for (int i = 0; i < ctrl[0]; i++) { if ((oldpos + i >= 0) && (oldpos + i < oldsize)) { newBuf[newpos + i] += oldBuf[oldpos + i]; } } newpos += ctrl[0]; oldpos += ctrl[0]; if (newpos + ctrl[1] > newsize) { throw new IOException("Corrupt by wrong patch file."); } if (!BSUtil.readFromStream(extraBlockIn, newBuf, newpos, ctrl[1])) { throw new IOException("Corrupt by wrong patch file."); } newpos += ctrl[1]; oldpos += ctrl[2]; } ctrlBlockIn.close(); diffBlockIn.close(); extraBlockIn.close(); return newBuf;}
资源文件合并
- 差分信息文件asset/res_meta.txt
- 合并后的文件res/resource.apk
- 差分记录中的大文件采用BsPatch算法合成
- 将没变的文件写到resource.apk文件中
- 将AndroidManifest.xml文件写到resource.apk文件中
- 将合并后的大文件写入resource.apk中
- 将新增的文件写入resource.apk中
- 将变更的小文件写入resource.apk中
- 写入注释out.setComment(oldApk.getComment());
- 验证合并后的resource.apk文件中的resources.asrc文件的MD5值是否与目的文件的MD5值相同
等待odex操作完成
com.tencent.tinker.lib.patch.DexDiffPatchInternalprotected static boolean waitAndCheckDexOptFile(File patchFile, Tinker manager) { if (optFiles.isEmpty()) { return true; } int size = optFiles.size() * 6; if (size > MAX_WAIT_COUNT) { size = MAX_WAIT_COUNT; } TinkerLog.i(TAG, "dex count: %d, final wait time: %d", optFiles.size(), size); for (int i = 0; i < size; i++) { if (!checkAllDexOptFile(optFiles, i + 1)) { try { Thread.sleep(WAIT_ASYN_OAT_TIME); } catch (InterruptedException e) { TinkerLog.e(TAG, "thread sleep InterruptedException e:" + e); } } } List<File> failDexFiles = new ArrayList<>(); // check again, if still can be found, just return for (File file : optFiles) { TinkerLog.i(TAG, "check dex optimizer file exist: %s, size %d", file.getName(), file.length()); if (!SharePatchFileUtil.isLegalFile(file)) { TinkerLog.e(TAG, "final parallel dex optimizer file %s is not exist, return false", file.getName()); failDexFiles.add(file); } } if (!failDexFiles.isEmpty()) { manager.getPatchReporter().onPatchDexOptFail(patchFile, failDexFiles, new TinkerRuntimeException(ShareConstants.CHECK_DEX_OAT_EXIST_FAIL)); return false; } if (Build.VERSION.SDK_INT >= 21) { Throwable lastThrowable = null; for (File file : optFiles) { TinkerLog.i(TAG, "check dex optimizer file format: %s, size %d", file.getName(), file.length()); int returnType; try { returnType = ShareElfFile.getFileTypeByMagic(file); } catch (IOException e) { // read error just continue continue; } if (returnType == ShareElfFile.FILE_TYPE_ELF) { ShareElfFile elfFile = null; try { elfFile = new ShareElfFile(file); } catch (Throwable e) { TinkerLog.e(TAG, "final parallel dex optimizer file %s is not elf format, return false", file.getName()); failDexFiles.add(file); lastThrowable = e; } finally { if (elfFile != null) { try { elfFile.close(); } catch (IOException ignore) { } } } } } if (!failDexFiles.isEmpty()) { Throwable returnThrowable = lastThrowable == null ? new TinkerRuntimeException(ShareConstants.CHECK_DEX_OAT_FORMAT_FAIL) : new TinkerRuntimeException(ShareConstants.CHECK_DEX_OAT_FORMAT_FAIL, lastThrowable); manager.getPatchReporter().onPatchDexOptFail(patchFile, failDexFiles, returnThrowable); return false; } } return true; }
善后
写入新补丁信息到文件
//com.tencent.tinker.loader.shareutil.SharePatchInfopublic static boolean rewritePatchInfoFileWithLock(File pathInfoFile, SharePatchInfo info, File lockFile) { ... File lockParentFile = lockFile.getParentFile(); boolean rewriteSuccess; ShareFileLockHelper fileLock = null; //获取文件排他锁 fileLock = ShareFileLockHelper.getFileLock(lockFile); rewriteSuccess = rewritePatchInfoFile(pathInfoFile, info); return rewriteSuccess; } private static boolean rewritePatchInfoFile(File pathInfoFile, SharePatchInfo info) { // 写入默认构建指纹 if (ShareTinkerInternals.isNullOrNil(info.fingerPrint)) { info.fingerPrint = Build.FINGERPRINT; } //写入默认oat目录 if (ShareTinkerInternals.isNullOrNil(info.oatDir)) { info.oatDir = DEFAULT_DIR; } boolean isWritePatchSuccessful = false; int numAttempts = 0;//尝试次数 File parentFile = pathInfoFile.getParentFile(); if (!parentFile.exists()) { parentFile.mkdirs(); } //尝试2次 while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isWritePatchSuccessful) { numAttempts++; Properties newProperties = new Properties(); newProperties.put(OLD_VERSION, info.oldVersion);//旧补丁版本 newProperties.put(NEW_VERSION, info.newVersion);//新补丁版本 newProperties.put(FINGER_PRINT, info.fingerPrint);//构建指纹 newProperties.put(OAT_DIR, info.oatDir);//oat目录 FileOutputStream outputStream = null; outputStream = new FileOutputStream(pathInfoFile, false); String comment = "from old version:" + info.oldVersion + " to new version:" + info.newVersion; //写入patch.info文件 newProperties.store(outputStream, comment); //再读取出来,验证是否写入成功 SharePatchInfo tempInfo = readAndCheckProperty(pathInfoFile); isWritePatchSuccessful = tempInfo != null && tempInfo.oldVersion.equals(info.oldVersion) && tempInfo.newVersion.equals(info.newVersion); if (!isWritePatchSuccessful) { pathInfoFile.delete(); } } if (isWritePatchSuccessful) { return true; } return false; }
杀死补丁合成进程:patch
删除旧的合成文件
重启主进程
Patch Load 补丁加载
由于Tinker代理了Application类,所以应用启动的Application类为TinkerApplication。
//com.tencent.tinker.loader.app.TinkerApplication//代理Application类public abstract class TinkerApplication extends Application //Application初始化调用此方法 @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); Thread.setDefaultUncaughtExceptionHandler(new TinkerUncaughtHandler(this)); onBaseContextAttached(base); }//代理attachBaseContext()方法private void onBaseContextAttached(Context base) { ... loadTinker(); ...}//加载Tinkerprivate void loadTinker() { tinkerResultIntent = new Intent(); try { //反射AbstractTinkerLoader类,因为此类可由开发者自定义。默认为com.tencent.tinker.loader.TinkerLoader Class<?> tinkerLoadClass = Class.forName(loaderClassName, false, getClassLoader()); //反射调用tryLoad(TinkerApplicaiton app)方法 Method loadMethod = tinkerLoadClass.getMethod(AbstractTinkerLoader, TinkerApplication.class); Constructor<?> constructor = tinkerLoadClass.getConstructor(); tinkerResultIntent = (Intent) loadMethod.invoke(constructor.newInstance(), this); } catch (Throwable e) { }}
准备
验证
验证文件合法性
验证patch-641e634c.apk是否包含对应的dex、so、res文件
验证合成后的补丁文件签名和TinkerId,同时读取出其中包含的文件meta.txt,
将新的补丁信息写回patch.info文件
检查是否超出安全模式次数
加载合成后的dex
//com.tencent.tinker.loader.TinkerDexLoaderpublic static boolean loadTinkerJars(final TinkerApplication application, String directory, //补丁版本目录,例patch-641e634c String oatDir, //dex优化后文件目录(patch-641e634c/odex Intent intentResult, //合并结果 boolean isSystemOTA //系统是否支持oat编译) { //获取默认的PatchClassLoader PathClassLoader classLoader = (PathClassLoader) TinkerDexLoader.class.getClassLoader(); //dex文件目录 patch-641e634c/dex/ String dexPath = directory + "/" + DEX_PATH + "/"; //如果开启的加载验证,会验证dex文件的MD5值是否与meta.txt文件中的记录是否一致 if (application.isTinkerLoadVerifyFlag()) { ... } //如果系统支持oat,则进行dex2oat操作。前面已经说了dex2oat的实现,这里不再重复。 if (isSystemOTA) { ... //这里关键是获取dex的指令集(arm|libs|x86),后面会再详细说明。 targetISA = ShareOatUtil.getOatFileInstructionSet(testOptDexFile); TinkerParallelDexOptimizer.optimizeAll( legalFiles, optimizeDir, **true**, targetISA, new TinkerParallelDexOptimizer.ResultCallback() { ); } //然后就是根据不同的系统版本,将新的dex数组插入到dexElements数组前面,实现错误修复。 SystemClassLoaderAdder.installDexes(application, classLoader, optimizeDir, legalFiles);}
DexElements操作
类查找原理
在程序运行时,使用到某个类时,虚拟机需要从dex文件中找出响应的类并加载到虚拟机,然后构造其实例对象供开发者使用。那么,虚拟机是如何找到需要的类的呢?答案就是PathClassLoader,PatchClassLoader继承自BaseDexClassLoader,可以加载指定路径的dex文件。BaseDexClassLoader包含一个类型为DexPathList成员变量pathList,顾名思义,DexPathList是包含多个Dex文件的列表,具体类查找过程由DexPathList执行。下面看BaseDexClassLoade的实现:
//dalvik.system.BaseDexClassLoaderpublic class BaseDexClassLoader extends ClassLoader { private final DexPathList pathList; //构造函数 public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) { super(parent); this.originalPath = dexPath; //新建pathList对象 this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory); } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { //类查找操作交给pathList Class clazz = pathList.findClass(name); if (clazz == null) { throw new ClassNotFoundException(name); } return clazz; }}
下面看DexPathList的类查找过程
//dalvik.system.DexPathListfinal class DexPathList { //dex元素数组 Element是DexPathList的内部静态类,组合了dex原文件、DexFile、ZipFile private final Element[] dexElements; //构造函数 public DexPathList(ClassLoader definingContext, String dexPath, String libraryPath, File optimizedDirectory) { this.definingContext = definingContext; //根据dex文件路径和odex目录,初始化dexElements this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory); this.nativeLibraryDirectories = splitLibraryPath(libraryPath); } //类查找 public Class findClass(String name) { //遍历dexElements数组,如果找到名称一致的类则返回。 for (Element element : dexElements) { DexFile dex = element.dexFile; if (dex != null) { //根据类名从dex加载 Class clazz = dex.loadClassBinaryName(name, definingContext); if (clazz != null) { return clazz; } } } return null; //由Dex文件创建Element数组,一般通过反射此方法生成补丁dex文件的Element private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory) { }}
所以类修复的原理就是,加载补丁中的dex,然后将新的dexElements数组插到旧的dexElements数组前面,这样再查找类的时候,就会优先从补丁dex文件寻找,达到动态修复的目的。其中需要反射修改的字段为BaseClassLoader中的pathList字段和DexPathList类中的dexElements字段。不过由于不同系统版本字段名称或方法参数不同,所以需要区分版本进行反射修改。
区分系统版本
//com.tencent.tinker.loader.SystemClassLoaderAdderpublic static void installDexes(Application application, PathClassLoader loader, File dexOptDir, List<File> files){ //对多个classes.dex排序 排序后:[classes.dex classes2.dex classes3.dex test.dex] files = createSortedAdditionalPathEntries(files); ClassLoader classLoader = loader; //在Android7.0以上系统,自定义ClassLoader if (Build.VERSION.SDK_INT >= 24 && !checkIsProtectedApp(files)) { classLoader = AndroidNClassLoader.inject(loader, application); } //然后根据不同系统版本,有不同的操作实现 if (Build.VERSION.SDK_INT >= 23) { V23.install(classLoader, files, dexOptDir); } else if (Build.VERSION.SDK_INT >= 19) { V19.install(classLoader, files, dexOptDir); } else if (Build.VERSION.SDK_INT >= 14) { V14.install(classLoader, files, dexOptDir); } else { V4.install(classLoader, files, dexOptDir); }}
在Android 7.0上,为了避免一个dex文件在多个ClassLoader中注册产生的异常,需要自定义 ClassLoader。也是为了避免AndroidN混合编译带来的影响。1、替换AndroidNClassLoader中的pathList字段为原始PathClassLoader中的
pathList字段,之后用AndroidNClassLoader做dexElements数组扩展操作。
2、然后还要替换掉 Context.mBase.mPackageInfo中持有的mClassLoader字段和当前线程的classLoader。
这样就保证后面所有类的加载都是用自定义的AndroidNClassLoader。
AndroidNClassLoader实现
//com.tencent.tinker.loader.AndroidNClassLoaderclass AndroidNClassLoader extends PathClassLoader { //构造方法 private AndroidNClassLoader(String dexPath, PathClassLoader parent, Application application) { super(dexPath, parent.getParent()); //保存原始ClassLoader,即加载TinkerDexLoader的类加载器 originClassLoader = parent; String name = application.getClass().getName(); if (name != null && !name.equals("android.app.Application")) { //保存Application类名称:com.tencent.tinker.loader.app.TinkerApplication applicationClassName = name; } } //自定义类查找规则 public Class<?> findClass(String name) throws ClassNotFoundException { // 与tinker.loader相关的类由默认的类加载器加载,包含TinkerApplication类 // 其他的类由此加载器加载 if ((name != null && name.startsWith("com.tencent.tinker.loader.") && !name.equals(SystemClassLoaderAdder.CHECK_DEX_CLASS)) || (applicationClassName != null&&applicationClassName.equals(name))){ return originClassLoader.loadClass(name); } return super.findClass(name); } //新建AndroidNClassLoader并注入到应用上下文中 public static AndroidNClassLoader inject(PathClassLoader originClassLoader, Application application) throws Exception { //新建AndroidNClassLoader AndroidNClassLoader classLoader = createAndroidNClassLoader(originClassLoader, application); //修改Context.mBase.mPackageInfo.mClassLoader和Thread持有的ClassLoader reflectPackageInfoClassloader(application, classLoader); return classLoader; } //反射修改修改Context.mBase.mPackageInfo.mClassLoader字段为AndroidNClassLoader //设置当前线程的ClassLoader为AndroidNClassLoader private static void reflectPackageInfoClassloader(Application application, ClassLoader reflectClassLoader) throws Exception { String defBase = "mBase"; String defPackageInfo = "mPackageInfo"; String defClassLoader = "mClassLoader"; Context baseContext = (Context) ShareReflectUtil.findField(application, defBase).get(application); Object basePackageInfo = ShareReflectUtil.findField(baseContext, defPackageInfo).get(baseContext); Field classLoaderField = ShareReflectUtil.findField(basePackageInfo, defClassLoader); Thread.currentThread().setContextClassLoader(reflectClassLoader); classLoaderField.set(basePackageInfo, reflectClassLoader); } 这里就不展开说明AndroidNClassLoader的新建过程了,主要步骤为从原始的ClassLoader中取出已经加载的DexFile、libFile文件名称列表,再构造出新的DexPathList,然后赋值给AndroidNClassLoader。
获取Dex指令集
dex2oat是将dex指令编译成本地机器指令,所以需要指定编译的指令集,应与当前机器的cpu指令集一致。基本原理为读取dex文件oat之后的ELF文件中的固定块的值,以此判断cpu指令集。代码实现为
//com.tencent.tinker.loader.shareutil.ShareOatUtilpublic static String getOatFileInstructionSet(File oatFile) throws Throwable { ... switch (InstructionSet.values()[isaNum]) { case kArm: case kThumb2: result = "arm"; break; case kArm64: result = "arm64"; break; case kX86: result = "x86"; break; case kX86_64: result = "x86_64"; break; case kMips: result = "mips"; break; case kMips64: result = "mips64"; break; case kNone: result = "none"; break; default: throw new IOException("Should not reach here."); }}
判断Cpu指令集无非是分析ELF文件格式,但是这个已经编译好的文件是从哪里来的?之前我们说过,在补丁合并时并没有做dex2oat操作,因为不知道具体机型的指令集。从源码里看是分析的test.dex.dex文件,不过这个文件只是在dex合并时进行了odex操作,留个坑~~~
test.dex测试dex是否插入成功
加载合成后的res
Res资源查找原理
在Android开发中,使用资源的方式分两种: 一种是使用res包下面压缩资源的,通过getResoures()返回的Resources访问。第二种是访问asset文件夹下的原始资源,通过getAsset()返回AssetManager访问。
Resouces资源查找
我们在开发是使用字符串或图片资源的时候,是通过getResources()方法获取到一个Resources对象,然后通过Resources获取各种资源的。无论是在Activity中,还是在Fragment中。在Fragment中获取Resoures时实际上是在基类中调用getActivity.getResources()间接获取到的。
//android.content.res.Resources//访问应用程序资源类。//存在的意义在于只能访问该应用级别的资源//并提供了一组从Assets中获取指定类型数据的高级APIpublic class Resources { ... final AssetManager mAssets; ... //以获取字符串资源为例 //可以看出Resouces类内部也是通过AssetManager查找的资源 public CharSequence getText(int id) throws NotFoundException { CharSequence res = mAssets.getResourceText(id); if (res != null) { return res; } }}
AssetManger资源查找
//android.content.res.AssetManager//该类提供了对应用程序原始资源的访问//该类提供了一个较低级别的Api,通过简单字节流的方式读取与应用绑定的原始文件public final class AssetManager implements AutoCloseable { //构造函数 //该构造函数通常不会被应用程序使用到,因为新创建的AssetManager仅仅包含基本的系统资源 //应用程序应该通过Resources.getAssets检索对应的资源管理 public AssetManager() { synchronized (this) { init(false); ensureSystemAssets(); } } //根据指定的标识符(R.String.id)获取字符串资源 final CharSequence getResourceText(int ident) { synchronized (this) { TypedValue tmpValue = mValue; int block = loadResourceValue(ident, (short) 0, tmpValue, true); if (block >= 0) { if (tmpValue.type == TypedValue.TYPE_STRING) { return mStringBlocks[block].get(tmpValue.data); } return tmpValue.coerceToString(); } } return null; } //向AssetManager中添加一个新的资源包路径 public final int addAssetPath(String path) { synchronized (this) { int res = addAssetPathNative(path); makeStringBlocks(mStringBlocks); return res; } }}
Resouces和AssetManager前生今世
那Activity中这个Resources是哪来的?通过Activity的继承关系可以得到答案。
Activity extends ContextThemeWrapper extends ContextWrapper extends Context。通过这个继承关系可以知道Activity就是一个Context,Context抽象类中有一个getResources()方法,可以获取到主线程Resources对象。为什么说是主线程的Resources对象,因为Activity是在主线程创建的嘛!
实际上,无论是ContextThemeWrapper和ContextWrapper,从类名可以看出来他们只是一个继承自Context的代理类,并没有具体实现。Context的真正实现是ContextImpl,并且通过ContextWrapper
的attachBaseContext(Context base)方法将代理对象赋值给ContextWrapper。所以Resources对象来自于ContextImpl。
下面看一下ContextImpl中mResoures对象的创建过程。```//android.app.ContextImpl//Context Api的通过实现,为Android四大组件提供了基础的Context对象 class ContextImpl extends Context { //和res资源查找相关的几个关键属性 final ActivityThread mMainThread; final LoadedApk mPackageInfo; private final ResourcesManager mResourcesManager; private final Resources mResources; //私有的构造函数 private ContextImpl(ContextImpl container, ActivityThread mainThread, LoadedApk packageInfo, IBinder activityToken, UserHandle user, boolean restricted,Display display, Configuration overrideConfiguration) { ... mOuterContext = this; mMainThread = mainThread; mPackageInfo = packageInfo; mResourcesManager = ResourcesManager.getInstance(); //从主线程中获取Resouces对象,从后面AssetManager的替换情况看,Tinker并没 有直接替换ContextImpl中的mResources属性,而是将ResourcesManager中的所有 Resources对象中的AssetManager替换掉。 Resources resources = packageInfo.getResources(mainThread); //如果resouces对象不为null,则通过mResoucesManager查找顶级的Resouces if (resources != null) { if (activityToken != null || displayId != Display.DEFAULT_DISPLAY || overrideConfiguration != null || (compatInfo != null && compatInfo.applicationScale != resources.getCompatibilityInfo().applicationScale)) { resources = mResourcesManager.getTopLevelResources( packageInfo.getResDir(), packageInfo.getSplitResDirs(), packageInfo.getOverlayDirs(), packageInfo.getApplicationInfo().sharedLibraryFiles, displayId, overrideConfiguration, compatInfo, activityToken); } } mResources = resources; } //获取AssetManager @Override public AssetManager getAssets() { return getResources().getAssets(); } //获取Resources @Override public Resources getResources() { return mResources; }}```
//android.app.LoadedApk//当前已经已经加载的Apk的本地状态public final class LoadedApk{ //Apk中资源目录 private final String mResDir;}
看到底,ContextImpl的创建过程:
//android.app.ActivityThread//在应用进程中,管理主线程的执行,调度和执行Activity、广播,还有ActivityManager其他的操作请求public final class ActivityThread { //ApplicaitonThread是一个Binder接口,用来进程间通信 final ApplicationThread mAppThread = new ApplicationThread(); // final ArrayMap<String, WeakReference<LoadedApk>> mPackages = new ArrayMap<String, WeakReference<LoadedApk>>(); // final ArrayMap<String, WeakReference<LoadedApk>> mResourcePackages = new ArrayMap<String, WeakReference<LoadedApk>>(); //什么鬼 private final ResourcesManager mResourcesManager; //该类有两种创建入口 //1、main()方法 //2、systemMain()方法 //新建ActivityThread对象,均调用attach(boolean isSystem)方法,区别就是当前应用是否是 //系统应用 ActivityThread() { mResourcesManager = ResourcesManager.getInstance(); } public static ActivityThread systemMain() { ... ActivityThread thread = new ActivityThread(); thread.attach(true); return thread; } public static void main(String[] args) { ... Looper.prepareMainLooper(); ActivityThread thread = new ActivityThread(); thread.attach(false); Looper.loop(); ... } private void attach(boolean system) { sCurrentActivityThread = this; mSystemThread = system; if (!system) { ... RuntimeInit.setApplicationObject(mAppThread.asBinder()); final IActivityManager mgr = ActivityManagerNative.getDefault(); try { //调用IActivityManager的attachApplication(ApplicationThread at)方法 mgr.attachApplication(mAppThread); } catch (RemoteException ex) { // Ignore } ... } else { } } //ActivityThread启动后,会有一系列的binder通信,告知当前进程启动一个Activity //1、Launcher通过Binder进程间通信机制通知ActivityManagerService,它要启动一个Activity; //2、ActivityManagerService通过Binder进程间通信机制通知Launcher进入Paused状态; //3、Launcher通过Binder进程间通信机制通知ActivityManagerService,它已经准备就绪进入Paused状态,于是ActivityManagerService就创建一个新的进程,用来启动一个ActivityThread实例,即将要启动的Activity就是在这个ActivityThread实例中运行; //4、ActivityThread通过Binder进程间通信机制将一个ApplicationThread类型的Binder对象传递给ActivityManagerService,以便以后ActivityManagerService能够通过这个Binder对象和它进行通信; //5、ActivityManagerService通过Binder进程间通信机制通知ActivityThread,现在一切准备就绪,它可以真正执行Activity的启动操作了。 //在启动Activity的时候会创建ContextImpl private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) { ... Activity activity = null; java.lang.ClassLoader cl = r.packageInfo.getClassLoader(); //新建Activity activity = mInstrumentation.newActivity( cl, component.getClassName(), r.intent); if (activity != null) { //创建ActivityContext Context appContext = createBaseContextForActivity(r, activity); ... //设置给新建的Activity activity.attach(appContext, this, getInstrumentation(), r.token, r.ident, app, r.intent, r.activityInfo, title, r.parent, r.embeddedID, r.lastNonConfigurationInstances, config, r.referrer, r.voiceInteractor); ... } } //为Activity创建ContextImpl private Context createBaseContextForActivity(ActivityClientRecord r, final Activity activity) { ContextImpl appContext = ContextImpl.createActivityContext(this, r.packageInfo, r.token); appContext.setOuterContext(activity); Context baseContext = appContext; ... return baseContext; }}
ResourceManager也需要反射的。
//android.app.ResourcesManagerpublic class ResourcesManager { //保存了应用中所有的Resources对象,如果没有加载外部的apk,则只有一个原始应用apk一个 Resources final ArrayMap<ResourcesKey, WeakReference<Resources> > mActiveResources = new ArrayMap<ResourcesKey, WeakReference<Resources> >();}
ApplicationINfo类也需要反射,为了解决WebView翻转的bug
//android.content.pm.ApplicationInfo//基础Apk的全路径类似:/base-1.apkpublic String sourceDir;//sourceDir目录的公共的可用的部分,包含资源文件、manifest//这个目录和sourceDir是不同的,如果应用是向前锁定的//个人理解这个目录是可以被其他进程访问的公开资源目录public String publicSourceDir;
综合上面获取应用资源的流程可以看出,如果想要替换已有的应用资,可以创建一个新的AssetManager对象,加载新的资源包路径。然后通过反射技术替换掉mResouces对象中持有的mAssets变量即可。
通过Resources resources = packageInfo.getResources(mainThread);可知,Resources是存储在LoadedApk类型的packageInfo实例中,所以最好也要把packageInfo中的Resources实例也替换掉。
Tinker加载合并res分析
//com.tencent.tinker.loader.TinkerResourceLoadepublic static boolean loadTinkerResources(TinkerApplication application, String directory, Intent intentResult) { //合成文件路径为/tinker/patch-641e634c/res/resources.apk String resourceString = directory + "/" + RESOURCE_PATH + "/"+RESOURCE_FILE; File resourceFile = new File(resourceString); //如果开启了加载验证,需要验证资源文件的MD5 if (application.isTinkerLoadVerifyFlag()) { } //由TinkerResourcePatcher执行资源文件加载 TinkerResourcePatcher.monkeyPatchExistingResources(application, resourceString); }
//com.tencent.tinker.loader.TinkerResourcePatcher//准备工作,各种反射public static void isResourceCanPatch(Context context) throws Throwable { //反射得到context中的activityThread对象 Class<?> activityThread = Class.forName("android.app.ActivityThread"); currentActivityThread = ShareReflectUtil.getActivityThread(context, activityThread); //加载LoadedApk类,并取到其中的resDir属性 Class<?> loadedApkClass; try { loadedApkClass = Class.forName("android.app.LoadedApk"); } catch (ClassNotFoundException e) { loadedApkClass = Class.forName("android.app.ActivityThread$PackageInfo"); } resDir = loadedApkClass.getDeclaredField("mResDir"); resDir.setAccessible(true); //取到ActivityThread的packagesFiled和resourcePackagesFiled属性 packagesFiled = activityThread.getDeclaredField("mPackages"); packagesFiled.setAccessible(true); resourcePackagesFiled = activityThread.getDeclaredField("mResourcePackages"); resourcePackagesFiled.setAccessible(true); //新建AssetManager,这里区分百度系统自定义的BaiduAssetManager AssetManager assets = context.getAssets(); // Baidu os if (assets.getClass().getName(). equals("android.content.res.BaiduAssetManager")) { Class baiduAssetManager = Class.forName("android.content.res.BaiduAssetManager"); newAssetManager = (AssetManager) baiduAssetManager.getConstructor().newInstance(); } else { newAssetManager = AssetManager.class.getConstructor().newInstance(); } //获取AssetManager的addAssetPath(String path)方法,方便后面添加补丁路径 addAssetPathMethod = AssetManager.class.getDeclaredMethod("addAssetPath", String.class); addAssetPathMethod.setAccessible(true); //反射获取AssetManager的ensureStringBlocks方法,4.4系统在调用addAssetPath方法后,需 要额外调用此方法 ensureStringBlocksMethod = AssetManager.class.getDeclaredMethod("ensureStringBlocks"); ensureStringBlocksMethod.setAccessible(true); //获取ResourcesManager中持有的所有可用的Resources对象,这些Resoures里面的 AssetManager也需要替换 //4.4系统前后的这个Resrouces列表所处位置不同 //4.4前直接在ActivityThread类中持有,名字是mActivityResoures,没有ResourcesManager //4.4后在ResourcesManager单例中,名字是mActiveResources(7.0之前)或mResourceReferences(7.0之后) if (SDK_INT >= KITKAT) { //4.4之后 //pre-N // Find the singleton instance of ResourcesManager Class<?> resourcesManagerClass = Class.forName("android.app.ResourcesManager"); Method mGetInstance = resourcesManagerClass.getDeclaredMethod("getInstance"); mGetInstance.setAccessible(true); Object resourcesManager = mGetInstance.invoke(null); try { Field fMActiveResources = resourcesManagerClass.getDeclaredField("mActiveResources"); fMActiveResources.setAccessible(true); ArrayMap<?, WeakReference<Resources>> activeResources19 = (ArrayMap<?, WeakReference<Resources>>) fMActiveResources.get(resourcesManager); references = activeResources19.values(); } catch (NoSuchFieldException ignore) { // N moved the resources to mResourceReferences Field mResourceReferences = resourcesManagerClass.getDeclaredField("mResourceReferences"); mResourceReferences.setAccessible(true); references = (Collection<WeakReference<Resources>>) mResourceReferences.get(resourcesManager); } } else { //4.4之前 Field fMActiveResources = activityThread.getDeclaredField("mActiveResources"); fMActiveResources.setAccessible(true); HashMap<?, WeakReference<Resources>> activeResources7 = (HashMap<?, WeakReference<Resources>>) fMActiveResources.get(currentActivityThread); references = activeResources7.values(); } //最后反射得到Resources的AssetManager字段,为后面替换做准备 //7.0之前该字段名称为mAsset,7.0之后改为mResourcesImpl if (SDK_INT >= 24) { try { // N moved the mAssets inside an mResourcesImpl field resourcesImplFiled = Resources.class.getDeclaredField("mResourcesImpl"); resourcesImplFiled.setAccessible(true); } catch (Throwable ignore) { // for safety assetsFiled = Resources.class.getDeclaredField("mAssets"); assetsFiled.setAccessible(true); } } else { assetsFiled = Resources.class.getDeclaredField("mAssets"); assetsFiled.setAccessible(true); }}//替换respublic static void monkeyPatchExistingResources(Context context, String externalResourceFile) throws Throwable { //1、替换LoadedApk中的resDir为合成后的资源文件目录,LoadedApk位于ActivityThread中类型 //为ArrayMap的mPackage和mResourcePackages字段中。 for (Field field : new Field[]{packagesFiled, resourcePackagesFiled}) { Object value = field.get(currentActivityThread); for (Map.Entry<String, WeakReference<?>> entry : ((Map<String, WeakReference<?>>) value).entrySet()) { Object loadedApk = entry.getValue().get(); if (loadedApk == null) { continue; } if (externalResourceFile != null) { resDir.set(loadedApk, externalResourceFile); } } } //2、为新建的AssetManager对象,添加新的资源路径 if (((Integer) addAssetPathMethod.invoke(newAssetManager, externalResourceFile)) == 0) { throw new IllegalStateException("Could not create new AssetManager"); } //3、确保安全,调用AssetManager的ensureStingBlocks()方法 ensureStringBlocksMethod.invoke(newAssetManager); //4、从ResourcesManager中取出所有Resources引用,并替换其中的mAssetManager对象 for (WeakReference<Resources> wr : references) { Resources resources = wr.get(); if (resources != null) { try { assetsFiled.set(resources, newAssetManager); } catch (Throwable ignore) { // N Object resourceImpl = resourcesImplFiled.get(resources); // for Huawei HwResourcesImpl Field implAssets = ShareReflectUtil.findField(resourceImpl, "mAssets"); implAssets.setAccessible(true); implAssets.set(resourceImpl, newAssetManager); } //清空预加载的类型数组问题 //Reource类有一个mTypedArrayPool属性,SynchronizedPool<TypedArray> mTypedArrayPool = new SynchronizedPool<TypedArray>(5); //在miui系统上,把TypedArray改成了MiuiTypesArray,然后从其中获取字符串, 而不是从AssetManager中获取,所以需要把mTypedArrayPool清空。 clearPreloadTypedArrayIssue(resources); resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics()); } } //5、问题规避:Android 7.0上,如果Activity包含一个WebView,当屏幕反转后,资源补丁会失效 //在5.x、6.x的机器上,发现了StatusBarNotification无法展开RemoteView异常 if (Build.VERSION.SDK_INT >= 24) { if (publicSourceDirField != null) { publicSourceDirField.set(context.getApplicationInfo(), externalResourceFile); } } //6、检测是否加载Res成功 if (!checkResUpdate(context)) { throw new TinkerRuntimeException(ShareConstants.CHECK_RES_INSTALL_FAIL); }}
总结:替换Res的思路是
1、新建AssetManager,并添加新的res文件路径。
2、需要替换的Resource对象是ResourceManager哈希表中存储的Resources列表
3、需要替换LoadedApk中的resDir
4、需要替换ApplicationInfo中publicSourceDir的值新的res目录
手动加载so文件
在加载补丁文件的时候,并不会像预加载Dex那样,直接加载so包,只是缓存下来了so补丁包的路径和对应的MD5值。例:lib/arm-v7/libtest.so : agadsgwee234ft354t
//com.tencent.tinker.loader.TinkerSoLoaderfor (ShareBsDiffPatchInfo info : libraryList) { String middle = info.path + "/" + info.name; libs.put(middle, info.md5);}
当需要加载补丁so文件时,可以通过TinkerApplicationHelper类实现。
//com.tencent.tinker.lib.tinker.TinkerApplicationHelperpublic static boolean loadLibraryFromTinker(ApplicationLike applicationLike, String relativePath, String libname) throws UnsatisfiedLinkError { ... System.load(patchLibraryPath);}
善后
如果是oat模式下,需要杀死其他进程。
if (oatModeChanged) { ShareTinkerInternals.killAllOtherProcess(app); Log.i(TAG, "tryLoadPatchFiles:oatModeChanged, try to kill all other process"); }
- tinker源码分析
- Tinker源码分析
- Tinker接入及源码分析(一)
- Tinker接入及源码分析(二)
- Tinker接入及源码分析(三)
- Tinker接入及源码分析(一)
- Tinker接入及源码分析(二)
- Tinker接入及源码分析(三)
- 热修复 tinker接入及源码分析
- 源码分析微信热修复框架Tinker的类加载过程
- 源码分析微信热修复框架Tinker的类加载过程 .
- 源码分析微信热修复框架Tinker的类加载过程
- Android 热修复 Tinker 源码分析之DexDiff / DexPatch
- Tinker原理分析
- Tinker接入及原理分析
- Tinker
- 源码阅读--腾讯Tinker热修复框架
- 微信Tinker的一切都在这里,包括源码(一)
- BZOJ 2648 SJY摆箱子
- SQL多表练习题
- mybatis系列教程(二)——spring整合mybatis
- 漫步最优化九——泰勒级数
- excel入门,如何玩转excel,你早该这么玩Excel笔记2
- Tinker源码分析
- COCOS滚动层显示图片
- LCT介绍
- JAVA 正则表达式 (超详细)
- 在VS中添加lib库的三种方法
- Xshell简易实现Linux跟Windows的文件互传
- 柳州一市民刮宝马车后留条,车主感动邀请肇事者到其公司上班(组图)
- 值栈ValueStack的原理与生命周期?
- Spring源码解析(一)