Tinker简单问答

为什么要替换 Application

看上一篇和 Instant Run 的对比,可能考虑不全

  • 确保 Multidex 开启
  • 保证应用 Application 类可修复
  • 7.0 混合编译的影响,要去替换掉原始 PathClassLoader 的加载功能

为什么还要在 dexElements 前面插入而不是直接替换?

因为 Tinker 是全量合成 dex ,比如在补丁前dex顺序是这样的:oldDex1 -> oldDex2 -> oldDex3..,那么假如修改了dex1中的文件,那么补丁顺序是这样的newDex1 -> oldDex1 -> oldDex2… 那为什么不直接使用newDex1去替换调oldDex呢?我觉得:

  • 运行期去替换调正在使用的dex是有风险的(也是我瞎猜)
  • 考虑到版本回退和以后的增量升级,在前面插入确实比替换更加方便安全

Tinker补丁构建走读

Tinker的补丁加载网上资料很多了,读起来也没太大难度,这里就不多说了。关于补丁构建的整个过程倒是不多,这里简单走读一下。

TinkerPatchSchemaTask

关于代码的变动 dex的patch 资源的patch

这个 Task 主要是用于 oldApk和 newApk 的差分,生成patch,包括dex,res,so的差分,主要起作用的类如下:

1
2
3
4
private final ManifestDecoder manifestDecoder;//检测是否增加了四大组件 关于dexMode的检测提示
private final UniqueDexDiffDecoder dexPatchDecoder;//dex 的patch
private final BsDiffDecoder soPatchDecoder;//so 的patch
private final ResDiffDecoder resPatchDecoder;//资源的patch

这个 Task 也是最关键的,可以从 ApkDecoder 这个类开始,首先会先把两个apk解压到build/outputs/tinkerPatch/{variant}/apkName目录下,在打patch 过程中tinkerPatch 目录下会生成很多过程文件,类似最终的patch apk,用于查看资源合成结果的 resources_out.zip,还有一下log.txt。关键的patch从

1
Files.walkFileTree(mNewApkDir.toPath(), new ApkFilesVisitor(config, mNewApkDir.toPath(), mOldApkDir.toPath(), dexPatchDecoder, soPatchDecoder, resPatchDecoder));

这里开始,遍历newApk解压后的目录,根据Pattern去匹配使用上面四个中哪个Decoder去处理这两个新旧文件。

  • dex查分

UniqueDexDiffDecoder dexPatchDecoder;处理dex开始,这里假如我们oldApk只有一个classes.dex,来到DexDiffDecoder.patch

1
2
//不是为了对比 是为了检查dex的一些规则
excludedClassModifiedChecker.checkIfExcludedClassWasModifiedInNewDex(oldFile, newFile);

这里去检查一下第一个dex中的一些限制,例如tinker的一些loader类一定要在第一个dex中等限制。
如果新增了一个dex

1
2
3
4
5
6
//new add file 在new apk中多了一个dex 直接复制
if (oldFile == null || !oldFile.exists() || oldFile.length() == 0) {
hasDexChanged = true;
copyNewDexAndLogToDexMeta(newFile, newMd5, dexDiffOut);
return true;
}

然后正常的修改了dex

1
2
3
4
5
/**
* Before starting real diff works, we collect added class descriptor
* and deleted class descriptor for further analysing in {@code checkCrossDexMovingClasses}.
*/
private void collectAddedOrDeletedClasses(File oldFile, File newFile)

把对应的dex保存在

1
2
// collect current old dex file and corresponding new dex file for further processing.
oldAndNewDexFilePairList.add(new AbstractMap.SimpleEntry<>(oldFile, newFile));

然后走到dexPatchDecoder.onAllPatchesEnd();//开始生成 保存patch文件

1
2
3
4
5
if (config.mIsProtectedApp) {//仅仅在加固的时候使用 只将变化的类合成补丁
generateChangedClassesDexFile();
} else {
generatePatchInfoFile();
}

然后来到这个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
try {
DexPatchGenerator dexPatchGen = new DexPatchGenerator(oldDexFile, newDexFile);
dexPatchGen.setAdditionalRemovingClassPatterns(config.mDexLoaderPattern);//查分时候排除dex.loader闭包中的类
logWriter.writeLineToInfoFile(
String.format(
"Start diff between [%s] as old and [%s] as new:",
getRelativeStringBy(oldDexFile, config.mTempUnzipOldDir),
getRelativeStringBy(newDexFile, config.mTempUnzipNewDir)
)
);
dexPatchGen.executeAndSaveTo(dexDiffOut);
} catch (Exception e) {
throw new TinkerPatchException(e);
}

这里就是具体的dexDiff算法了,我也看不懂,就当个黑盒,反正会生产处一个查分dex保存,然后后面还会在合成一个全量dex用来查看,保存一下log日志之类的,大致的dex查分就完成了。

  • res 查分
    回到遍历newApk目录的地方,还是根据Pattern去匹配资源文件,在这个方法中进行查分
1
2
//处理修改的资源
private boolean dealWithModeFile(String name, String newMd5, File oldFile, File newFile, File outputFile)

还是使用了BSD进行差分,输出log,修改的文件复制到tinker_result的目录下。然后到onAllPatchesEnd,和dex一样,还是会生成全量的resources_out.zip去查看合成结果,还有log输出。

  • so查分
    这个也是使用了BSD查分,没太多好说的。

TinkerManifestTask

自动添加/修改tinker_id,运行时检查补丁版本

TinkerResourceIdTask

R.txt 的保存 ids.xml public.xml的处理,用于处理资源改变时ID变动问题,将基础包的R.txt处理成ids.xml,public.xml,保存到intermediates/res/merged/{variant}/values/下,用于打包时候的资源ID分配。保证资源ID的不变动。

TinkerProguardConfigTask

混淆的处理,主要的作用是将tinker中默认的混淆信息和基准包的mapping信息加入混淆列表。

TinkerMultidexConfigTask

主要将dex.loader中配置的class也keep进main dex.