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.CliMain
public 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.Runner
protected 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.ApkDecoder
public 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.ApkDecoder
public 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.ManifestDecoder
public 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.BsDiffDecoder
public 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,26a3339220be96e865fc523fe4a162a8
pattern:3
resources.arsc
res/*
assets/*
large modify:1
resources.arsc,26a3339220be96e865fc523fe4a162a8,946796371
modify:1
res/layout/activity_bug.xml
add:199
res/drawable-hdpi-v4/abc_ab_share_pack_mtrl_alpha.9.png
res/drawable-hdpi-v4/abc_btn_check_to_on_mtrl_000.png
res/drawable-hdpi-v4/abc_btn_check_to_on_mtrl_015.png
res/drawable-hdpi-v4/abc_btn_radio_to_on_mtrl_000.png
res/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.DexPatchGernerator
public 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.AbstractPatch
public 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.ShareSecurityCheck
public 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.ShareDexDiffPatchInfo public final String rawName; //dex原始文件名称,例如classes2.dex public 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或jar public 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.DexSectionPatchAlgorithm
private 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.DexDiffPatchInternal
private 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.TinkerParallelDexOptimizer
public static boolean optimizeAll(Collection<File> dexFiles, File optimizedDir, ResultCallback cb) {
return optimizeAll(dexFiles, optimizedDir, **false**, null, cb);
}
//此处useInterpretMode参数一直为false
public 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.OptimizeWorker
private 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.DexDiffPatchInternal
protected 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.SharePatchInfo
public 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();
...
}
//加载Tinker
private 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.TinkerDexLoader
public 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.BaseDexClassLoader
public 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.DexPathList
final 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.SystemClassLoaderAdder
public 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.AndroidNClassLoader
class 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.ShareOatUtil
public 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中获取指定类型数据的高级API
public 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.ResourcesManager
public 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.apk
public 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.TinkerResourceLoade
public 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);
}
}
//替换res
public 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.TinkerSoLoader
for (ShareBsDiffPatchInfo info : libraryList) {
String middle = info.path + "/" + info.name;
libs.put(middle, info.md5);
}
当需要加载补丁so文件时,可以通过TinkerApplicationHelper类实现。
//com.tencent.tinker.lib.tinker.TinkerApplicationHelper
public 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");
}