本系列 Tinker 源碼解析基于 Tinker v1.9.12
前面講到了 Tinker 安裝補(bǔ)丁的流程,現(xiàn)在就詳細(xì)地來看下 dex 合成的代碼阀溶。代碼入口就在 DexDiffPatchInternal.tryRecoverDexFiles 中品追。
UpgradePatch
//we use destPatchFile instead of patchFile, because patchFile may be deleted during the patch process
if (!DexDiffPatchInternal.tryRecoverDexFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {
TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch dex failed");
return false;
}
直接調(diào)用了 DexDiffPatchInternal.tryRecoverDexFiles 方法玄括。
tryRecoverDexFiles
protected static boolean tryRecoverDexFiles(Tinker manager, ShareSecurityCheck checker, Context context,
String patchVersionDirectory, File patchFile) {
// 檢查是否開啟支持dex補(bǔ)丁開關(guān)
if (!manager.isEnabledForDex()) {
TinkerLog.w(TAG, "patch recover, dex is not enabled");
return true;
}
// 檢查補(bǔ)丁包中的 dex_meta.txt 是否存在
String dexMeta = checker.getMetaContentMap().get(DEX_META_FILE);
if (dexMeta == null) {
TinkerLog.w(TAG, "patch recover, dex is not contained");
return true;
}
long begin = SystemClock.elapsedRealtime();
// 到這個方法中執(zhí)行具體的操作
boolean result = patchDexExtractViaDexDiff(context, patchVersionDirectory, dexMeta, patchFile);
long cost = SystemClock.elapsedRealtime() - begin;
TinkerLog.i(TAG, "recover dex result:%b, cost:%d", result, cost);
return result;
}
tryRecoverDexFiles 方法開頭做了些校驗(yàn),最后又到 patchDexExtractViaDexDiff 中诵盼。
patchDexExtractViaDexDiff
private static boolean patchDexExtractViaDexDiff(Context context, String patchVersionDirectory, String meta, final File patchFile) {
// dex補(bǔ)丁合成的路徑
String dir = patchVersionDirectory + "/" + DEX_PATH + "/";
// extractDexDiffInternals 這個方法是重點(diǎn);莶颉!风宁!
if (!extractDexDiffInternals(context, dir, meta, patchFile, TYPE_DEX)) {
TinkerLog.w(TAG, "patch recover, extractDiffInternals fail");
return false;
}
// 把 tinker/patch-xxxxx/dex/ 下面的文件校驗(yàn)下,看看是否是合法的dex文件
File dexFiles = new File(dir);
File[] files = dexFiles.listFiles();
List<File> legalFiles = new ArrayList<>();
// may have directory in android o
if (files != null) {
for (File file : files) {
final String fileName = file.getName();
if (file.isFile()
&& (fileName.endsWith(ShareConstants.DEX_SUFFIX)
|| fileName.endsWith(ShareConstants.JAR_SUFFIX)
|| fileName.endsWith(ShareConstants.PATCH_SUFFIX))
) {
legalFiles.add(file);
}
}
}
TinkerLog.i(TAG, "legal files to do dexopt: " + legalFiles);
// 對 dex 做 opt 優(yōu)化
final String optimizeDexDirectory = patchVersionDirectory + "/" + DEX_OPTIMIZE_PATH + "/";
return dexOptimizeDexFiles(context, legalFiles, optimizeDexDirectory, patchFile);
}
在 patchDexExtractViaDexDiff 中可以看到蛹疯, dex 文件合成之后戒财,會對其做 opt 優(yōu)化。而合成的代碼就在 extractDexDiffInternals 里面捺弦。
extractDexDiffInternals 方法有點(diǎn)長饮寞。按照老規(guī)矩,我們分段來看列吼。
extractDexDiffInternals
private static boolean extractDexDiffInternals(Context context, String dir, String meta, File patchFile, int type) {
// 讀取 dex_meta.txt 中的信息
patchList.clear();
ShareDexDiffPatchInfo.parseDexDiffPatchInfo(meta, patchList);
if (patchList.isEmpty()) {
TinkerLog.w(TAG, "extract patch list is empty! type:%s:", ShareTinkerInternals.getTypeString(type));
return true;
}
首先讀取 dex_meta.txt 中的信息幽崩,用“,”分割,保存到 patchList 中寞钥。
下面貼出一份 dex_meta.txt 的示例:
classes.dex,,1a6e6d6a40eff95aa33ab06e07acd413,1a6e6d6a40eff95aa33ab06e07acd413,d865f383455abd6e3f70096109543644,2999635299,712828526,jar
test.dex,,56900442eb5b7e1de45449d0685e6e00,56900442eb5b7e1de45449d0685e6e00,0,0,0,jar
dex_meta.txt 記錄著
- name :補(bǔ)丁 dex 名字
- path :補(bǔ)丁 dex 路徑
- destMd5InDvm :合成新 dex 在 dvm 中的 md5 值
- destMd5InArt :合成新 dex 在 art 中的 md5 值
- dexDiffMd5 :補(bǔ)丁包 dex 文件的 md5 值
- oldDexCrc :基準(zhǔn)包中對應(yīng) dex 的 crc 值
- newDexCrc :合成新 dex 的 crc 值
- dexMode :dex 類型慌申,為 jar 類型
接著往下看。
File directory = new File(dir);
if (!directory.exists()) {
directory.mkdirs();
}
//I think it is better to extract the raw files from apk
Tinker manager = Tinker.with(context);
ZipFile apk = null;
ZipFile patch = null;
try {
ApplicationInfo applicationInfo = context.getApplicationInfo();
if (applicationInfo == null) {
// Looks like running on a test Context, so just return without patching.
TinkerLog.w(TAG, "applicationInfo == null!!!!");
return false;
}
// 獲取到基準(zhǔn)包apk的路徑
String apkPath = applicationInfo.sourceDir;
// 基準(zhǔn)包文件
apk = new ZipFile(apkPath);
// 補(bǔ)丁包文件
patch = new ZipFile(patchFile);
if (checkClassNDexFiles(dir)) {
TinkerLog.w(TAG, "class n dex file %s is already exist, and md5 match, just continue", ShareConstants.CLASS_N_APK_NAME);
return true;
}
然后獲取基本包和補(bǔ)丁包的路徑理郑,為下面合成做準(zhǔn)備蹄溉。
// 遍歷 ShareDexDiffPatchInfo
for (ShareDexDiffPatchInfo info : patchList) {
long start = System.currentTimeMillis();
// 補(bǔ)丁dex文件路徑
final String infoPath = info.path;
String patchRealPath;
if (infoPath.equals("")) {
patchRealPath = info.rawName;
} else {
patchRealPath = info.path + "/" + info.rawName;
}
String dexDiffMd5 = info.dexDiffMd5;
String oldDexCrc = info.oldDexCrC;
// 如果是 dvm 虛擬機(jī)環(huán)境,但是補(bǔ)丁dex是art環(huán)境的您炉,就跳過
if (!isVmArt && info.destMd5InDvm.equals("0")) {
TinkerLog.w(TAG, "patch dex %s is only for art, just continue", patchRealPath);
continue;
}
String extractedFileMd5 = isVmArt ? info.destMd5InArt : info.destMd5InDvm;
// 檢查 md5 值
if (!SharePatchFileUtil.checkIfMd5Valid(extractedFileMd5)) {
TinkerLog.w(TAG, "meta file md5 invalid, type:%s, name: %s, md5: %s", ShareTinkerInternals.getTypeString(type), info.rawName, extractedFileMd5);
manager.getPatchReporter().onPatchPackageCheckFail(patchFile, BasePatchInternal.getMetaCorruptedCode(type));
return false;
}
File extractedFile = new File(dir + info.realName);
// 如果合成的dex文件已經(jīng)存在了
if (extractedFile.exists()) {
// 就校驗(yàn)合成的 dex 文件md5值柒爵,如果通過就跳過
if (SharePatchFileUtil.verifyDexFileMd5(extractedFile, extractedFileMd5)) {
//it is ok, just continue
TinkerLog.w(TAG, "dex file %s is already exist, and md5 match, just continue", extractedFile.getPath());
continue;
} else {
TinkerLog.w(TAG, "have a mismatch corrupted dex " + extractedFile.getPath());
// 否則刪除文件
extractedFile.delete();
}
} else {
extractedFile.getParentFile().mkdirs();
}
從這里開始,就是遍歷 patchList 中的記錄赚爵,進(jìn)行一個個 dex 文件合成了棉胀。一開頭會去校驗(yàn)合成的文件是否存在,存在的話就跳過冀膝,進(jìn)行下一個唁奢。
ZipEntry patchFileEntry = patch.getEntry(patchRealPath);
ZipEntry rawApkFileEntry = apk.getEntry(patchRealPath);
if (oldDexCrc.equals("0")) {
if (patchFileEntry == null) {
TinkerLog.w(TAG, "patch entry is null. path:" + patchRealPath);
manager.getPatchReporter().onPatchTypeExtractFail(patchFile, extractedFile, info.rawName, type);
return false;
}
//it is a new file, but maybe we need to repack the dex file
if (!extractDexFile(patch, patchFileEntry, extractedFile, info)) {
TinkerLog.w(TAG, "Failed to extract raw patch file " + extractedFile.getPath());
manager.getPatchReporter().onPatchTypeExtractFail(patchFile, extractedFile, info.rawName, type);
return false;
}
}
如果 oldDexCrc 為0,就說明基準(zhǔn)包中對應(yīng)的 oldDex 文件不存在畸写,直接按照 patch 信息重新打包 dex 即可驮瞧。
// 如果 dexDiffMd5 為 0, 就說明補(bǔ)丁包中沒有這個dex枯芬,但是基準(zhǔn)包中存在
else if (dexDiffMd5.equals("0")) {
// skip process old dex for real dalvik vm
// 如果是 dvm 環(huán)境的無須做處理
if (!isVmArt) {
continue;
}
// 檢查基準(zhǔn)包中的 dex 是否為空
if (rawApkFileEntry == null) {
TinkerLog.w(TAG, "apk entry is null. path:" + patchRealPath);
manager.getPatchReporter().onPatchTypeExtractFail(patchFile, extractedFile, info.rawName, type);
return false;
}
//check source crc instead of md5 for faster
// 檢查基準(zhǔn)包中的 dex 的 crc 值和 dex_meta.txt 中是否一致
String rawEntryCrc = String.valueOf(rawApkFileEntry.getCrc());
if (!rawEntryCrc.equals(oldDexCrc)) {
TinkerLog.e(TAG, "apk entry %s crc is not equal, expect crc: %s, got crc: %s", patchRealPath, oldDexCrc, rawEntryCrc);
manager.getPatchReporter().onPatchTypeExtractFail(patchFile, extractedFile, info.rawName, type);
return false;
}
// Small patched dex generating strategy was disabled, we copy full original dex directly now.
//patchDexFile(apk, patch, rawApkFileEntry, null, info, smallPatchInfoFile, extractedFile);
// 直接復(fù)制 :copy full original dex directly now.
extractDexFile(apk, rawApkFileEntry, extractedFile, info);
// 復(fù)制完后校驗(yàn)一下md5值是否一致
if (!SharePatchFileUtil.verifyDexFileMd5(extractedFile, extractedFileMd5)) {
TinkerLog.w(TAG, "Failed to recover dex file when verify patched dex: " + extractedFile.getPath());
manager.getPatchReporter().onPatchTypeExtractFail(patchFile, extractedFile, info.rawName, type);
SharePatchFileUtil.safeDeleteFile(extractedFile);
return false;
}
}
上面這段代碼用來處理基準(zhǔn)包中有 oldDex 论笔,但是補(bǔ)丁包中沒有 dex 的情況采郎。
如果是 dvm 環(huán)境就跳過不處理即可,如果是 art 環(huán)境就把 oldDex 復(fù)制過去狂魔。
else {
// 檢查補(bǔ)丁包中 dex 是否存在
if (patchFileEntry == null) {
TinkerLog.w(TAG, "patch entry is null. path:" + patchRealPath);
manager.getPatchReporter().onPatchTypeExtractFail(patchFile, extractedFile, info.rawName, type);
return false;
}
// 檢查補(bǔ)丁包中的 dex md5值是否合法
if (!SharePatchFileUtil.checkIfMd5Valid(dexDiffMd5)) {
TinkerLog.w(TAG, "meta file md5 invalid, type:%s, name: %s, md5: %s", ShareTinkerInternals.getTypeString(type), info.rawName, dexDiffMd5);
manager.getPatchReporter().onPatchPackageCheckFail(patchFile, BasePatchInternal.getMetaCorruptedCode(type));
return false;
}
// 檢查基準(zhǔn)包中的 dex 是否存在
if (rawApkFileEntry == null) {
TinkerLog.w(TAG, "apk entry is null. path:" + patchRealPath);
manager.getPatchReporter().onPatchTypeExtractFail(patchFile, extractedFile, info.rawName, type);
return false;
}
// 檢查基準(zhǔn)包中的 dex 的 crc 值是否一致
String rawEntryCrc = String.valueOf(rawApkFileEntry.getCrc());
if (!rawEntryCrc.equals(oldDexCrc)) {
TinkerLog.e(TAG, "apk entry %s crc is not equal, expect crc: %s, got crc: %s", patchRealPath, oldDexCrc, rawEntryCrc);
manager.getPatchReporter().onPatchTypeExtractFail(patchFile, extractedFile, info.rawName, type);
return false;
}
// 執(zhí)行合成操作
patchDexFile(apk, patch, rawApkFileEntry, patchFileEntry, info, extractedFile);
// 檢查合成出來的dex的 md5 值是否一致
if (!SharePatchFileUtil.verifyDexFileMd5(extractedFile, extractedFileMd5)) {
TinkerLog.w(TAG, "Failed to recover dex file when verify patched dex: " + extractedFile.getPath());
manager.getPatchReporter().onPatchTypeExtractFail(patchFile, extractedFile, info.rawName, type);
SharePatchFileUtil.safeDeleteFile(extractedFile);
return false;
}
TinkerLog.w(TAG, "success recover dex file: %s, size: %d, use time: %d",
extractedFile.getPath(), extractedFile.length(), (System.currentTimeMillis() - start));
}
}
if (!mergeClassNDexFiles(context, patchFile, dir)) {
return false;
}
} catch (Throwable e) {
throw new TinkerRuntimeException("patch " + ShareTinkerInternals.getTypeString(type) + " extract failed (" + e.getMessage() + ").", e);
} finally {
SharePatchFileUtil.closeZip(apk);
SharePatchFileUtil.closeZip(patch);
}
return true;
}
最后蒜埋,就是基準(zhǔn)包和補(bǔ)丁包中都存在對應(yīng) dex 的情況了。
代碼一開始就是一堆的各種校驗(yàn)最楷,都通過后整份,調(diào)用 patchDexFile 執(zhí)行合成操作。合成完后再對合成的 dex 進(jìn)行md5校驗(yàn)籽孙。
patchDexFile
private static void patchDexFile(
ZipFile baseApk, ZipFile patchPkg, ZipEntry oldDexEntry, ZipEntry patchFileEntry,
ShareDexDiffPatchInfo patchInfo, File patchedDexFile) throws IOException {
InputStream oldDexStream = null;
InputStream patchFileStream = null;
try {
// 基準(zhǔn)包 dex 文件輸入流
oldDexStream = new BufferedInputStream(baseApk.getInputStream(oldDexEntry));
// 補(bǔ)丁包 dex 文件輸入流
patchFileStream = (patchFileEntry != null ? new BufferedInputStream(patchPkg.getInputStream(patchFileEntry)) : null);
final boolean isRawDexFile = SharePatchFileUtil.isRawDexFile(patchInfo.rawName);
if (!isRawDexFile || patchInfo.isJarMode) {
ZipOutputStream zos = null;
try {
// 合成 dex 文件的輸出流
zos = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(patchedDexFile)));
zos.putNextEntry(new ZipEntry(ShareConstants.DEX_IN_JAR));
// Old dex is not a raw dex file.
if (!isRawDexFile) {
ZipInputStream zis = null;
try {
zis = new ZipInputStream(oldDexStream);
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
if (ShareConstants.DEX_IN_JAR.equals(entry.getName())) break;
}
if (entry == null) {
throw new TinkerRuntimeException("can't recognize zip dex format file:" + patchedDexFile.getAbsolutePath());
}
new DexPatchApplier(zis, patchFileStream).executeAndSaveTo(zos);
} finally {
StreamUtil.closeQuietly(zis);
}
} else {
new DexPatchApplier(oldDexStream, patchFileStream).executeAndSaveTo(zos);
}
zos.closeEntry();
} finally {
StreamUtil.closeQuietly(zos);
}
} else {
new DexPatchApplier(oldDexStream, patchFileStream).executeAndSaveTo(patchedDexFile);
}
} finally {
StreamUtil.closeQuietly(oldDexStream);
StreamUtil.closeQuietly(patchFileStream);
}
}
在 patchDexFile 中烈评,拿到基準(zhǔn)包 dex 文件的 InputStream 和補(bǔ)丁包 dex 文件的 InputStream ,然后利用 DexPatchApplier 把這兩個流合成一個 dex 文件犯建。
public void executeAndSaveTo(OutputStream out) throws IOException {
// Before executing, we should check if this patch can be applied to
// old dex we passed in.
byte[] oldDexSign = this.oldDex.computeSignature(false);
if (oldDexSign == null) {
throw new IOException("failed to compute old dex's signature.");
}
if (this.patchFile == null) {
throw new IllegalArgumentException("patch file is null.");
}
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)
)
);
}
// Firstly, set sections' offset after patched, sort according to their offset so that
// the dex lib of aosp can calculate section size.
TableOfContents patchedToc = this.patchedDex.getTableOfContents();
patchedToc.header.off = 0;
patchedToc.header.size = 1;
patchedToc.mapList.size = 1;
patchedToc.stringIds.off
= this.patchFile.getPatchedStringIdSectionOffset();
patchedToc.typeIds.off
= this.patchFile.getPatchedTypeIdSectionOffset();
patchedToc.typeLists.off
= this.patchFile.getPatchedTypeListSectionOffset();
patchedToc.protoIds.off
= this.patchFile.getPatchedProtoIdSectionOffset();
patchedToc.fieldIds.off
= this.patchFile.getPatchedFieldIdSectionOffset();
patchedToc.methodIds.off
= this.patchFile.getPatchedMethodIdSectionOffset();
patchedToc.classDefs.off
= this.patchFile.getPatchedClassDefSectionOffset();
patchedToc.mapList.off
= this.patchFile.getPatchedMapListSectionOffset();
patchedToc.stringDatas.off
= this.patchFile.getPatchedStringDataSectionOffset();
patchedToc.annotations.off
= this.patchFile.getPatchedAnnotationSectionOffset();
patchedToc.annotationSets.off
= this.patchFile.getPatchedAnnotationSetSectionOffset();
patchedToc.annotationSetRefLists.off
= this.patchFile.getPatchedAnnotationSetRefListSectionOffset();
patchedToc.annotationsDirectories.off
= this.patchFile.getPatchedAnnotationsDirectorySectionOffset();
patchedToc.encodedArrays.off
= this.patchFile.getPatchedEncodedArraySectionOffset();
patchedToc.debugInfos.off
= this.patchFile.getPatchedDebugInfoSectionOffset();
patchedToc.codes.off
= this.patchFile.getPatchedCodeSectionOffset();
patchedToc.classDatas.off
= this.patchFile.getPatchedClassDataSectionOffset();
patchedToc.fileSize
= this.patchFile.getPatchedDexSize();
Arrays.sort(patchedToc.sections);
patchedToc.computeSizesFromOffsets();
// Secondly, run patch algorithms according to sections' dependencies.
this.stringDataSectionPatchAlg = new StringDataSectionPatchAlgorithm(
patchFile, oldDex, patchedDex, oldToPatchedIndexMap
);
this.typeIdSectionPatchAlg = new TypeIdSectionPatchAlgorithm(
patchFile, oldDex, patchedDex, oldToPatchedIndexMap
);
this.protoIdSectionPatchAlg = new ProtoIdSectionPatchAlgorithm(
patchFile, oldDex, patchedDex, oldToPatchedIndexMap
);
this.fieldIdSectionPatchAlg = new FieldIdSectionPatchAlgorithm(
patchFile, oldDex, patchedDex, oldToPatchedIndexMap
);
this.methodIdSectionPatchAlg = new MethodIdSectionPatchAlgorithm(
patchFile, oldDex, patchedDex, oldToPatchedIndexMap
);
this.classDefSectionPatchAlg = new ClassDefSectionPatchAlgorithm(
patchFile, oldDex, patchedDex, oldToPatchedIndexMap
);
this.typeListSectionPatchAlg = new TypeListSectionPatchAlgorithm(
patchFile, oldDex, patchedDex, oldToPatchedIndexMap
);
this.annotationSetRefListSectionPatchAlg = new AnnotationSetRefListSectionPatchAlgorithm(
patchFile, oldDex, patchedDex, oldToPatchedIndexMap
);
this.annotationSetSectionPatchAlg = new AnnotationSetSectionPatchAlgorithm(
patchFile, oldDex, patchedDex, oldToPatchedIndexMap
);
this.classDataSectionPatchAlg = new ClassDataSectionPatchAlgorithm(
patchFile, oldDex, patchedDex, oldToPatchedIndexMap
);
this.codeSectionPatchAlg = new CodeSectionPatchAlgorithm(
patchFile, oldDex, patchedDex, oldToPatchedIndexMap
);
this.debugInfoSectionPatchAlg = new DebugInfoItemSectionPatchAlgorithm(
patchFile, oldDex, patchedDex, oldToPatchedIndexMap
);
this.annotationSectionPatchAlg = new AnnotationSectionPatchAlgorithm(
patchFile, oldDex, patchedDex, oldToPatchedIndexMap
);
this.encodedArraySectionPatchAlg = new StaticValueSectionPatchAlgorithm(
patchFile, oldDex, patchedDex, oldToPatchedIndexMap
);
this.annotationsDirectorySectionPatchAlg = new AnnotationsDirectorySectionPatchAlgorithm(
patchFile, oldDex, patchedDex, oldToPatchedIndexMap
);
this.stringDataSectionPatchAlg.execute();
this.typeIdSectionPatchAlg.execute();
this.typeListSectionPatchAlg.execute();
this.protoIdSectionPatchAlg.execute();
this.fieldIdSectionPatchAlg.execute();
this.methodIdSectionPatchAlg.execute();
this.annotationSectionPatchAlg.execute();
this.annotationSetSectionPatchAlg.execute();
this.annotationSetRefListSectionPatchAlg.execute();
this.annotationsDirectorySectionPatchAlg.execute();
this.debugInfoSectionPatchAlg.execute();
this.codeSectionPatchAlg.execute();
this.classDataSectionPatchAlg.execute();
this.encodedArraySectionPatchAlg.execute();
this.classDefSectionPatchAlg.execute();
// Thirdly, write header, mapList. Calculate and write patched dex's sign and checksum.
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();
// Finally, write patched dex to file.
this.patchedDex.writeTo(out);
}
而 DexPatchApplier 里面合流操作的代碼是需要根據(jù) Tinker 的 DexDiff 算法來的讲冠。大致就是把兩個 Dex 文件的每個分區(qū)做 merge 操作。
這里先留一個坑适瓦。等以后把 DexDiff 算法看明白了再補(bǔ)上竿开。
另外,dodola 寫了一篇 Tinker Dexdiff算法解析玻熙,有需要的同學(xué)可以看下否彩。
那么 dex 合成的流程就到這吧。