Tinker的patch生成方式有兩種棉姐,一種是通過命令行,一種是通過gradle的方式掰茶。下面我們簡單介紹下gradle的方式阎毅。這篇文章包括一下幾部分
patch文件的組成
生成流程
patch文件的生成
dex的差分
so的差分
生成meta,版本文件等
涉及目錄
Patch打包的涉及的目錄是tinker-build屈梁。其中命令行涉及的目錄是tinker-patch-cli嗤练, gradle涉及的目錄是tinker-patch-gradle-plugin。命令行和gradle只是入口不一樣在讶,而patch生成的代碼主要在tinker-patch-lib目錄中煞抬。tinker中dex的差分是微信自己實(shí)現(xiàn)的一套dexdiff算法,差分的算法代碼主要在tinker-commons中构哺,其中DexPatchApplier是進(jìn)行生成patch的入口革答。那么其他文件的差分呢,比如so,resource文件的差分是采用的什么算法呢遮婶?
patch文件的組成
changed_classes.dex
META_INF
----XXX.RSA
----XXX.SF
----XXX.MF
test.dex
assets
----package_meta.txt
----dex_meta.txt
YAPATCH.MF
其中changed_classes.dex和test.dex是dex蝗碎。test.dex是為了驗(yàn)證dex的加載是否成功湖笨,test.dex中含有com.tencent.tinker.loader.TinkerTestDexLoad類旗扑,該類中包含一個(gè)字段isPatch,補(bǔ)丁的加載校驗(yàn)是在SystemClassLoaderAdder中的checkDexInstall方法慈省。
checkDexInstall就是通過findField該字段判斷是否加載成功臀防。
需要注意的是這個(gè)test.dex是從目錄下直接拷貝的,而不是直接生成的边败,test.dex中只有一個(gè)TinkerTestDexLoad類袱衷,而且其中的屬性isPatch是true。因此如果沒有加載patch,就會直接加載打包進(jìn)apk中的TinkerTestDexLoad笑窜,此時(shí)的isPatch屬性為false.如果加載補(bǔ)丁成功致燥,就會從patch中的class.dex中讀取TinkerTestDexLoad這個(gè)類,而class.dex中TinkerTestDexLoad的屬性isPatch是true排截。因此可以使用讀取patch的值來作為補(bǔ)丁是否加載成功的依據(jù)嫌蚤。
而changed_classes.dex可能因?yàn)楦膭哟a的范圍而生成多個(gè)changed_class。 根據(jù)不同的情況,最多有四個(gè)文件是以meta.txt結(jié)尾的:
package_meta.txt 補(bǔ)丁包的基本信息
dex_meta.txt dex補(bǔ)丁的信息
so_meta.txt so補(bǔ)丁的信息
res_meta.txt 資源補(bǔ)丁的信息
package_meta.txt中的格式范例如下:
#base package config field
#Tue Jun 25 15:32:59 CST 2019
NEW_TINKER_ID=XXXXXX-patch
TINKER_ID=XXXXXX-base
而dex_meta.txt中的格式范例如下:
changed_classes.dex,,5d4ce4b80d4d5168006a63a5a16d94b3,5d4ce4b80d4d5168006a63a5a16d94b3,0,0,0,jar
test.dex,,56900442eb5b7e1de45449d0685e6e00,56900442eb5b7e1de45449d0685e6e00,0,0,0,jar
而res_meta.txt文件的格式范例如下:
resources_out.zip,4019114434,6148149bd5ed4e0c2f5357c6e2c577d6
pattern:4
resources.arsc
r/*
res/*
assets/*
modify:1
r/g/ag.xml
add:1
assets/only_use_to_test_tinker_resource.txt
生成流程
下面分析比較常用的gradle方法生成patch的流程断傲。gradle插件的入口是TinkerPatchPlugin脱吱。其中調(diào)用了多個(gè)task,名字及類型如下所示:
tinkerPatch${variantName} 類型:TinkerPatchSchemaTask
tinkerProcess${variantName}Manifest 類型:TinkerManifestTask
tinkerProcess${variantName}ResourceId 類型:TinkerResourceIdTask
tinkerProcess${variantName}Proguard 類型:TinkerProguardConfigTask
tinkerProcess${variantName}MultidexKeep 類型:TinkerMultidexConfigTask
我們先重點(diǎn)看下tinkerPatch${variantName}這個(gè)task认罩,這個(gè)task是TinkerPatchSchemaTask類型箱蝠,在TinkerPatchPlugin的setPatchNewApkPath方法中有如下一句代碼:
tinkerPatchBuildTask.dependsOn variant.assemble
因此在對應(yīng)的variant執(zhí)行完assemble之后,就會執(zhí)行tinkerPatch${variantName}。
// com.tencent.tinker.build.gradle.task.TinkerPatchSchemaTask
@TaskAction
def tinkerPatch() {
//開始打包patch
configuration.checkParameter()
configuration.buildConfig.checkParameter()
configuration.res.checkParameter()
configuration.dex.checkDexMode()
configuration.sevenZip.resolveZipFinalPath()
InputParam.Builder builder = new InputParam.Builder()
if (configuration.useSign) {
if (signConfig == null) {
throw new GradleException("can't the get signConfig for this build")
}
builder.setSignFile(signConfig.storeFile)
.setKeypass(signConfig.keyPassword)
.setStorealias(signConfig.keyAlias)
.setStorepass(signConfig.storePassword)
}
// patch的參數(shù)宦搬,從tinker.gradle中讀取配置信息
builder.setOldApk(configuration.oldApk)
.setNewApk(buildApkPath)
.setOutBuilder(outputFolder)
.setIgnoreWarning(configuration.ignoreWarning)
.setAllowLoaderInAnyDex(configuration.allowLoaderInAnyDex)
.setRemoveLoaderForAllDex(configuration.removeLoaderForAllDex)
.setDexFilePattern(new ArrayList<String>(configuration.dex.pattern))
.setIsProtectedApp(configuration.buildConfig.isProtectedApp)// note isProtectedApp,是在tinker.gradle中配置
.setIsComponentHotplugSupported(configuration.buildConfig.supportHotplugComponent)
.setDexLoaderPattern(new ArrayList<String>(configuration.dex.loader))
.setDexIgnoreWarningLoaderPattern(new ArrayList<String>(configuration.dex.ignoreWarningLoader))
.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))
.setResourceIgnoreChangeWarningPattern(new ArrayList<String>(configuration.res.ignoreChangeWarning))
.setResourceLargeModSize(configuration.res.largeModSize)
.setUseApplyResource(configuration.buildConfig.usingResourceMapping)
.setConfigFields(new HashMap<String, String>(configuration.packageConfig.getFields()))
.setSevenZipPath(configuration.sevenZip.path)
.setUseSign(configuration.useSign)
.setArkHotPath(configuration.arkHot.path)
.setArkHotName(configuration.arkHot.name)
InputParam inputParam = builder.create()
Runner.gradleRun(inputParam);
}
這個(gè)方法首先從tinker.gradle中讀取相關(guān)配置牙瓢,然后作為參數(shù),開始調(diào)用Runner.gradleRun方法開始準(zhǔn)備生成patch文件间校。gradleRun中調(diào)用來run方法一罩,run中直接調(diào)用來tinkerPatch, 這個(gè)方法就真正開始創(chuàng)建patch撇簿。下面我們看下這個(gè)方法聂渊,代碼如下:
// com.tencent.tinker.build.patch.Runner
protected void tinkerPatch() {
Logger.d("-----------------------Tinker patch begin-----------------------");
Logger.d(mConfig.toString());
try {
//gen patch
ApkDecoder decoder = new ApkDecoder(mConfig);
decoder.onAllPatchesStart();
decoder.patch(mConfig.mOldApkFile, mConfig.mNewApkFile);
decoder.onAllPatchesEnd();
//gen meta file and version file
PatchInfo info = new PatchInfo(mConfig);
info.gen();
//build patch
PatchBuilder builder = new PatchBuilder(mConfig);
builder.buildPatch();
} catch (Throwable e) {
goToError(e, ERRNO_USAGE);
}
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", mConfig.mOutFolder);
Logger.d("-----------------------Tinker patch end-------------------------");
}
前面講過patch文件的組成,在tinkerPatch也是分幾步生成四瘫,首先生成dex等patch文件汉嗽,然后生成meta和版本等文件,最后將前兩步生成的文件打包成patch找蜜。
patch文件的生成
下面我們先重點(diǎn)看下patch的第一部分的生成饼暑。 ApkDecoder的構(gòu)造方法如下:
// com.tencent.tinker.build.decoder.ApkDecoder
public ApkDecoder(Configuration config) throws IOException {
super(config);
this.mNewApkDir = config.mTempUnzipNewDir;
this.mOldApkDir = config.mTempUnzipOldDir;
this.manifestDecoder = new ManifestDecoder(config);
//put meta files in assets
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);
arkHotDecoder = new ArkHotDecoder(config, prePath + TypedValue.ARKHOT_META_TXT);
Logger.d("config: " + config.mArkHotPatchPath + " " + config.mArkHotPatchName + prePath + TypedValue.ARKHOT_META_TXT);
resDuplicateFiles = new ArrayList<>();
}
ApkDecoder有ManifestDecoder,UniqueDexDiffDecoder洗做,BsDiffDecoder弓叛,ResDiffDecoder,ArkHotDecoder類型的多個(gè)Decoder诚纸,在構(gòu)造方法將這幾個(gè)成員變量初始化撰筷。 各個(gè)Decoder分別針對代碼中不同的部分進(jìn)行patch。都是繼承自BaseDecoder畦徘,然后實(shí)現(xiàn)了patch毕籽,onAllPatchesStart,onAllPatchesEnd這個(gè)三個(gè)抽象方法井辆。 創(chuàng)建了ApkDecoder實(shí)例decoder后关筒,調(diào)用onAllPatchesStart進(jìn)行patch之前的準(zhǔn)備工作。onAllPatchesStart代碼如下:
@Override
public void onAllPatchesStart() throws IOException, TinkerPatchException {
manifestDecoder.onAllPatchesStart();
dexPatchDecoder.onAllPatchesStart();
soPatchDecoder.onAllPatchesStart();
resPatchDecoder.onAllPatchesStart();
}
隨后每個(gè)decoder分別執(zhí)行自己的onAllPatchesStart方法杯缺。調(diào)用onAllPatchesStart方法后蒸播,就調(diào)用ApkDecoder的patch方法開始生成patch。patch方法的代碼如下:
// com.tencent.tinker.build.decoder.ApkDecoder
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();
arkHotDecoder.onAllPatchesEnd();
//clean resources
dexPatchDecoder.clean();
soPatchDecoder.clean();
resPatchDecoder.clean();
arkHotDecoder.clean();
return true;
}
按照代碼中的注釋萍肆,先檢查manifest文件的改動袍榆。然后調(diào)用Files.walkFileTree來生成patch。我們先看下manifestDecoder的patch的文件是如何檢查manifest文件的匾鸥,代碼入下:
// com.tencent.tinker.build.decoder.ManifestDecoder
public boolean patch(File oldFile, File newFile) throws IOException, TinkerPatchException {
try {
AndroidParser oldAndroidManifest = AndroidParser.getAndroidManifest(oldFile);
AndroidParser newAndroidManifest = AndroidParser.getAndroidManifest(newFile);
// Android版本低于14直接返回蜡塌,此處忽略
final String oldXml = oldAndroidManifest.xml.trim();
final String newXml = newAndroidManifest.xml.trim();
final boolean isManifestChanged = !oldXml.equals(newXml);
f (!isManifestChanged) {
Logger.d("\nManifest has no changes, skip rest decode works.");
return false;
}
...... 省略對manifest變化的處理。
}
}
這部分的邏輯是先調(diào)用AndroidParser的getAndroidManifest方法從文件中讀取到manifest的內(nèi)容勿负,然后拿出其中的xml進(jìn)行比較馏艾。如果沒有變化劳曹,直接返回。一般情況下manifest的變化都是新增四大組件導(dǎo)致的琅摩, 而熱更比較少新增铁孵,因此manifest變化的情況先跳過。另外manifest的修改了房资,后續(xù)進(jìn)行patch的合成加載進(jìn)行相應(yīng)的處理蜕劝。 回到ApkDecoder的patch方法繼續(xù)看,將要打包patch的兩個(gè)apk解壓后轰异。然后調(diào)用Files的walkFileTree方法來遍歷新的apk解壓后的目錄岖沛。這個(gè)Files是NIO中的類,walkFileTree方法傳遞兩個(gè)參數(shù)搭独,一個(gè)是 要遍歷的目錄婴削,第二個(gè)參數(shù)是遍歷行為控制器FileVisitor,它是一個(gè)接口牙肝,里面定義了4個(gè)方法用來指定當(dāng)你訪問一個(gè)節(jié)點(diǎn)之前唉俗、之中、之后配椭、失敗時(shí)應(yīng)該采取什么行動虫溜。這里看下ApkFilesVisitor這個(gè)類,實(shí)現(xiàn)了visitFile方法股缸,制定訪問文件時(shí)的操作行為衡楞。的構(gòu)造方法代碼如下:
// com.tencent.tinker.build.decoder.ApkDecoder
ApkFilesVisitor(Configuration config, Path newPath, Path oldPath, BaseDecoder dex, BaseDecoder so, BaseDecoder resDecoder) {
this.config = config;
this.dexDecoder = dex;
this.soDecoder = so;
this.resDecoder = resDecoder;
this.newApkPath = newPath;
this.oldApkPath = oldPath;
}
將在ApkDecoder構(gòu)造方法中初始化的各種decoder傳遞進(jìn)來,在遍歷目錄針對不同類型的文件調(diào)用不同的decoder乓序。下面看下遍歷文件visitFile這個(gè)方法寺酪,代碼如下:
com.tencent.tinker.build.decoder.ApkDecoder
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Path relativePath = newApkPath.relativize(file);
Path oldPath = oldApkPath.resolve(relativePath);
File oldFile = null;
//is a new file?!
if (oldPath.toFile().exists()) {
oldFile = oldPath.toFile();
}
String patternKey = relativePath.toString().replace("\\", "/");
if (Utils.checkFileInPattern(config.mDexFilePattern, patternKey)) {//mDexFilePattern "classes*.dex""classes*.dex"
//also treat duplicate file as unchanged
if (Utils.checkFileInPattern(config.mResFilePattern, patternKey) && oldFile != null) {
resDuplicateFiles.add(oldFile);
}
try {
dexDecoder.patch(oldFile, file.toFile());
} catch (Exception e) {
throw new RuntimeException(e);
}
return FileVisitResult.CONTINUE;
}
if (Utils.checkFileInPattern(config.mSoFilePattern, patternKey)) {//mSoFilePattern "lib/*/*.so"
//also treat duplicate file as unchanged
if (Utils.checkFileInPattern(config.mResFilePattern, patternKey) && oldFile != null) {
resDuplicateFiles.add(oldFile);
}
try {
soDecoder.patch(oldFile, file.toFile());
} catch (Exception e) {
throw new RuntimeException(e);
}
return FileVisitResult.CONTINUE;
}
if (Utils.checkFileInPattern(config.mResFilePattern, patternKey)) {// mResFilePattern “res/*", "r/*", "assets/*", "resources.arsc"
try {
resDecoder.patch(oldFile, file.toFile());
} catch (Exception e) {
throw new RuntimeException(e);
}
return FileVisitResult.CONTINUE;
}
return FileVisitResult.CONTINUE;
}
針對符合config.mDexFilePattern命名規(guī)則的文件調(diào)用UniqueDexDiffDecoder進(jìn)行patch坎背,對符合config.mSoFilePattern命名規(guī)則的文件調(diào)用BsDiffDecoder進(jìn)行patch替劈,對符合config.mResFilePattern命名要求的文件調(diào)用ResDiffDecoder進(jìn)行patch合成。mDexFilePattern得滤,mSoFilePattern陨献,mResFilePattern這結(jié)果值是在tinker.gradle中進(jìn)行的配置,一般使用默認(rèn)就行懂更。下面先對UniqueDexDiffDecoder的patch進(jìn)行分析眨业,從名字上來看,我們離差分操作不遠(yuǎn)了沮协。
dex的差分
dex文件的查分操作在UniqueDexDiffDecoder的patch方法中龄捡,UniqueDexDiffDecoder繼承自DexDiffDecoder。UniqueDexDiffDecoder的patch操作主要是調(diào)用父類的patch操作慷暂,之后對文件名進(jìn)行重名判斷聘殖。我們主要看下DexDiffDecoder的patch方法,代碼如下:
// com.tencent.tinker.build.decoder.DexDiffDecoder
@Override
public boolean patch(final File oldFile, final File newFile) throws IOException, TinkerPatchException {
final String dexName = getRelativeDexName(oldFile, newFile);
// first of all, we should check input files if excluded classes were modified.
Logger.d("Check for loader classes in dex: %s", dexName);
try {
excludedClassModifiedChecker.checkIfExcludedClassWasModifiedInNewDex(oldFile, newFile);
} catch (IOException e) {
throw new TinkerPatchException(e);
} catch (TinkerPatchException e) {
// 省略異常處理
}
// If corresponding new dex was completely deleted, just return false.
// don't process 0 length dex
if (newFile == null || !newFile.exists() || newFile.length() == 0) {
return false;
}
File dexDiffOut = getOutputPath(newFile).toFile();
final String newMd5 = getRawOrWrappedDexMD5(newFile);
//new add file
if (oldFile == null || !oldFile.exists() || oldFile.length() == 0) {
hasDexChanged = true;
copyNewDexAndLogToDexMeta(newFile, newMd5, dexDiffOut);
return true;
}
final String oldMd5 = getRawOrWrappedDexMD5(oldFile);
if ((oldMd5 != null && !oldMd5.equals(newMd5)) || (oldMd5 == null && newMd5 != null)) {
hasDexChanged = true;
if (oldMd5 != null) {
collectAddedOrDeletedClasses(oldFile, newFile);
}
}
RelatedInfo relatedInfo = new RelatedInfo();
relatedInfo.oldMd5 = oldMd5;
relatedInfo.newMd5 = newMd5;
// collect current old dex file and corresponding new dex file for further processing.
oldAndNewDexFilePairList.add(new AbstractMap.SimpleEntry<>(oldFile, newFile));
dexNameToRelatedInfoMap.put(dexName, relatedInfo);
return true;
}
patch這個(gè)方法主要是做一些準(zhǔn)備工作,準(zhǔn)備工作很繁瑣奸腺,需要處理的情況真多餐禁。而進(jìn)行差分工作主要在DexDiffDecoder的onAllPatchesEnd方法中,代碼如下
// com.tencent.tinker.build.decoder.DexDiffDecoder
@Override
public void onAllPatchesEnd() throws Exception {
if (!hasDexChanged) {
Logger.d("No dexes were changed, nothing needs to be done next.");
return;
}
// Whether tinker should treat the base apk as the one being protected by app
// protection tools.
// If this attribute is true, the generated patch package will contain a
// dex including all changed classes instead of any dexdiff patch-info files.
if (config.mIsProtectedApp) {
generateChangedClassesDexFile();
} else {
generatePatchInfoFile();
}
addTestDex();
}
可以看到針對是否是加固的APP突照,有不同的處理帮非。如果是加固的APP,則產(chǎn)生的package會包含所有的改動文件讹蘑。而對于非加固的文件末盔,只需要進(jìn)行dexdiff算法后生成的patch-info文件。這部分的說明來自TinkerBuildConfigExtension中對于的isProtectedApp的注釋座慰。generateChangedClassesDexFile這個(gè)方法使用的是dexlib2庫中的DexBuilder來生成dex文件庄岖。而generatePatchInfoFile這個(gè)方法使用的是DexPatchApplier,這個(gè)類是使用微信自研的dexDiff算法角骤。對于這個(gè)算法隅忿,簡單的介紹和示范在Android 熱修復(fù) Tinker 源碼分析之DexDiff / DexPatch,全面詳細(xì)的介紹在DexDiff。這個(gè)地方其實(shí)有點(diǎn)意外邦尊,因?yàn)橐恢币詾閐ex的patch是使用微信自研的dexDiff算法背桐,沒想到加固的APP并不是這套算法,而是dexlib2這個(gè)框架蝉揍。dex的patch這部分看了一天链峭,看得很暈,感謝厲害的大佬們又沾,以及更厲害的微信弊仪。
so的差分
so的patch是在BsDiffDecoder的patch中進(jìn)行,
// com.tencent.tinker.build.decoder.BsDiffDecoder
@Override
public boolean patch(File oldFile, File newFile) throws IOException, TinkerPatchException {
//first of all, we should check input files
if (newFile == null || !newFile.exists()) {
return false;
}
//new add file
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;
}
//both file length is 0
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;
}
//new add file
String oldMd5 = MD5.getMD5(oldFile);
if (oldMd5.equals(newMd5)) {
return false;
}
if (!bsDiffFile.getParentFile().exists()) {
bsDiffFile.getParentFile().mkdirs();
}
BSDiff.bsdiff(oldFile, newFile, bsDiffFile);
if (Utils.checkBsDiffFileSize(bsDiffFile, newFile)) {
writeLogFiles(newFile, oldFile, bsDiffFile, newMd5);
} else {
FileOperation.copyFileUsingStream(newFile, bsDiffFile);
writeLogFiles(newFile, null, null, newMd5);
}
return true;
}
如果是新增的資源文件杖刷,則直接拷貝励饵。否則使用BSDiff進(jìn)行diff操作,這是二進(jìn)制的差量算法滑燃,具體的介紹在這里BSDiff算法. 如果BSDiff.bsdiff方法生成的文件過大役听,就會直接當(dāng)做新增加的文件來對待,以避免patch時(shí)間過長表窘。
resource的差分
資源文件的diff在ResDiffDecoder這個(gè)類的patch方法中完成典予,采用的方法也是BSDiff算法。牽扯到知識真多乐严,真是功夫在詩外瘤袖。和so一樣,如果新增昂验,則直接拷貝捂敌。否則調(diào)用BSDiff算法昭娩,如果生成的文件過大,就直接當(dāng)做新增來處理黍匾。不過資源文件還需要處理AndroidManifest.xml和resources.arsc這兩個(gè)文件栏渺。
以上是dex,so,resource文件的diff,針對不同的情況和場景有好幾種算法锐涯,只能說微信確實(shí)做到了極致磕诊,厲害了微信。這些算法現(xiàn)在功力不夠纹腌,只能先跳過霎终。另外就是如果看的仔細(xì),還會發(fā)現(xiàn)ApkDecoder中有另外一個(gè)的一個(gè)ArkHotDecoder升薯,這個(gè)貌似是華為的方舟編譯器莱褒。
生成meta,版本文件等
完成了dex,so,resource文件的diff之后涎劈,回到Runner的tinkerPatch方法广凸,還有剩下的產(chǎn)生meta,版本文件和打包成patch文件兩部分。meta文件指的是package_meta.txt蛛枚,這個(gè)文件會將Configuration中一些信息輸出到文件中谅海,以方便在后面進(jìn)行補(bǔ)丁的合成時(shí)進(jìn)行信息校驗(yàn)。生成patch文件的方法在PatchBuilder的buildPatch方法中蹦浦,代碼如下:
// public void buildPatch() throws Exception {
final File resultDir = config.mTempResultDir;
if (!resultDir.exists()) {
throw new IOException(String.format(
"Missing patch unzip files, path=%s\n", resultDir.getAbsolutePath()));
}
//no file change
if (resultDir.listFiles().length == 0) {
return;
}
generateUnsignedApk(unSignedApk);
signApk(unSignedApk, signedApk);
use7zApk(signedApk, signedWith7ZipApk, sevenZipOutPutDir);
......
}
第一步在進(jìn)行dex,so,resource文件的diff時(shí)扭吁,將diff文件輸出到目錄下,然后調(diào)用generateUnsignedApk進(jìn)行patch文件的壓縮盲镶,之后對壓縮后的文件再進(jìn)行簽名侥袜。
至此,我們大體上完成了tinker的patch文件的生成溉贿,雖然是囫圇吞棗枫吧,有很多流程也沒有分析。最大的意外是加固模式下dex的算法竟然不是dexDiff顽照,和一直以來想的都不一樣由蘑。但萬里長征第一步,我們大體上了解了patch文件的生成代兵。tinker框架還有g(shù)radle插件部分,以及patch的合成以及加載爷狈。我們后續(xù)再詳細(xì)分析植影。技術(shù)水平有限,有錯誤的地方清不吝指出涎永,感謝思币。
參考文獻(xiàn)
感謝tinker的開源以及先行者的無私分享
Android熱更新開源項(xiàng)目Tinker源碼解析系列之一:Dex熱更新
Tinker源碼分析
Android 熱修復(fù) Tinker 源碼分析之DexDiff / DexPatch
Android動態(tài)資源加載原理和應(yīng)用