Android热修复技术-Tinker源码分析

2024-06-15 09:48

本文主要是介绍Android热修复技术-Tinker源码分析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

源码版本:1.9.14.3

一、使用Patch合成新的dex

我们收到后台返回的补丁包后合成调用的是如下代码:

TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), 
Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed_7zip.apk");

Environment.getExternalStorageDirectory().getAbsolutePath() + “/patch_signed_7zip.apk”;是补丁包的路径,补丁包名字是patch_signed_7zip.apk。
onReceiveUpgradePatch()的源码如下:

public static void onReceiveUpgradePatch(Context context, String patchLocation) {Tinker.with(context).getPatchListener().onPatchReceived(patchLocation);
}

点进onPatchReceived()源码如下:

public interface PatchListener {int onPatchReceived(String path);
}

是一个接口方法,我们要找到其实现类的方法。
默认的实现类是DefaultPatchListener,DefaultPatchListener中onPatchReceived()源码如下:

public int onPatchReceived(String path) {final File patchFile = new File(path);final String patchMD5 = SharePatchFileUtil.getMD5(patchFile);final int returnCode = patchCheck(path, patchMD5);if (returnCode == ShareConstants.ERROR_PATCH_OK) {runForgService();TinkerPatchService.runPatchService(context, path);} else {Tinker.with(context).getLoadReporter().onLoadPatchListenerReceiveFail(new File(path), returnCode);}return returnCode;
}

patchCheck(path, patchMD5);对tinker的相关配置(isEnable)以及patch的合法性进行检测,如果合法,则调用TinkerPatchService.runPatchService(context, path);
runPatchService()的源码如下:

public static void runPatchService(final Context context, final String path) {TinkerLog.i(TAG, "run patch service...");Intent intent = new Intent(context, TinkerPatchService.class);intent.putExtra(PATCH_PATH_EXTRA, path);intent.putExtra(RESULT_CLASS_EXTRA, resultServiceClass.getName());try {context.startService(intent);Log.e("tag", "runPatchService>>startService");} catch (Throwable thr) {TinkerLog.e(TAG, "run patch service fail, exception:" + thr);}
}

TinkerPatchService是一个IntentService,runPatchService()其实就是启动这个服务。因为是IntentService,接下来就会执行onHandleIntent()回调方法。onHandleIntent()源码如下:

protected void onHandleIntent(@Nullable Intent intent) {increasingPriority();doApplyPatch(this, intent);
}

注意:TinkerPatchService在manifest.xml中注册的代码如下:

<serviceandroid:name=".service.TinkerPatchService"android:exported="false"android:permission="android.permission.BIND_JOB_SERVICE"android:process=":patch" />

android:process=":patch"是为TinkerPatchService开一个新的进程。这时如果要看后面的日志,需要在AS中切换下进程,否则看不到日志。 被这个问题看不到日志纠结了一天,所以不光要看Java代码,manifest.xml中的内容也很重要的。
我们继续上面的源码,increasingPriority()的源码如下:

private void increasingPriority() {if (Build.VERSION.SDK_INT >= 26) {TinkerLog.i(TAG, "for system version >= Android O, we just ignore increasingPriority "+ "job to avoid crash or toasts.");return;}if ("ZUK".equals(Build.MANUFACTURER)) {TinkerLog.i(TAG, "for ZUK device, we just ignore increasingPriority "+ "job to avoid crash.");return;}TinkerLog.i(TAG, "try to increase patch process priority");try {Notification notification = new Notification();if (Build.VERSION.SDK_INT < 18) {startForeground(notificationId, notification);} else {startForeground(notificationId, notification);// start InnerServicestartService(new Intent(this, InnerService.class));}} catch (Throwable e) {TinkerLog.i(TAG, "try to increase patch process priority error:" + e);}
}

这里主要是利用系统的一个漏洞来启动一个前台Service。可以参考此文:关于 Android 进程保活,你所需要知道的一切。
doApplyPatch()的源码如下:

private static void doApplyPatch(Context context, Intent intent) {TinkerLog.e("tag", "doApplyPatch>>");// Since we may retry with IntentService, we should prevent// racing here again.if (!sIsPatchApplying.compareAndSet(false, true)) {TinkerLog.w(TAG, "TinkerPatchService doApplyPatch is running by another runner.");return;}Tinker tinker = Tinker.with(context);tinker.getPatchReporter().onPatchServiceStart(intent);if (intent == null) {TinkerLog.e(TAG, "TinkerPatchService received a null intent, ignoring.");return;}String path = getPatchPathExtra(intent);if (path == null) {TinkerLog.e(TAG, "TinkerPatchService can't get the path extra, ignoring.");return;}File patchFile = new File(path);long begin = SystemClock.elapsedRealtime();boolean result;long cost;Throwable e = null;PatchResult patchResult = new PatchResult();try {if (upgradePatchProcessor == null) {throw new TinkerRuntimeException("upgradePatchProcessor is null.");}result = upgradePatchProcessor.tryPatch(context, path, patchResult);} catch (Throwable throwable) {e = throwable;result = false;tinker.getPatchReporter().onPatchException(patchFile, e);}cost = SystemClock.elapsedRealtime() - begin;tinker.getPatchReporter().onPatchResult(patchFile, result, cost);patchResult.isSuccess = result;patchResult.rawPatchFilePath = path;patchResult.costTime = cost;patchResult.e = e;AbstractResultService.runResultService(context, patchResult, getPatchResultExtra(intent));sIsPatchApplying.set(false);
}

这里主要调用upgradePatchProcessor.tryPatch(context, path, patchResult);这又是一个接口方法,我们找到实现类是UpgradePatch,UpgradePatch中的tryPatch()源码如下:

public boolean tryPatch(Context context, String tempPatchPath, PatchResult patchResult) {Tinker manager = Tinker.with(context);final File patchFile = new File(tempPatchPath);......(进行一些校验)// it is a new patch, we first delete if there is any files// don't delete dir for faster retry// SharePatchFileUtil.deleteDir(patchVersionDirectory);final String patchName = SharePatchFileUtil.getPatchVersionDirectory(patchMd5);final String patchVersionDirectory = patchDirectory + "/" + patchName;TinkerLog.i(TAG, "UpgradePatch tryPatch:patchVersionDirectory:%s", patchVersionDirectory);//copy fileFile destPatchFile = new File(patchVersionDirectory + "/" + SharePatchFileUtil.getPatchVersionFile(patchMd5));try {// check md5 firstif (!patchMd5.equals(SharePatchFileUtil.getMD5(destPatchFile))) {// 1、复制补丁文件到程序的私有区域SharePatchFileUtil.copyFileUsingStream(patchFile, destPatchFile);TinkerLog.w(TAG, "UpgradePatch copy patch file, src file: %s size: %d, dest file: %s size:%d", patchFile.getAbsolutePath(), patchFile.length(),destPatchFile.getAbsolutePath(), destPatchFile.length());}} catch (IOException e) {TinkerLog.e(TAG, "UpgradePatch tryPatch:copy patch file fail from %s to %s", patchFile.getPath(), destPatchFile.getPath());manager.getPatchReporter().onPatchTypeExtractFail(patchFile, destPatchFile, patchFile.getName(), ShareConstants.TYPE_PATCH_FILE);return false;}//we use destPatchFile instead of patchFile, because patchFile may be deleted during the patch process// 2、合成dexif (!DexDiffPatchInternal.tryRecoverDexFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch dex failed");return false;}if (!ArkHotDiffPatchInternal.tryRecoverArkHotLibrary(manager, signatureCheck,context, patchVersionDirectory, destPatchFile)) {return false;}if (!BsDiffPatchInternal.tryRecoverLibraryFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch library failed");return false;}if (!ResDiffPatchInternal.tryRecoverResourceFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch resource failed");return false;}// check dex opt file at last, some phone such as VIVO/OPPO like to change dex2oat to interpretedif (!DexDiffPatchInternal.waitAndCheckDexOptFile(patchFile, manager)) {TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, check dex opt file failed");return false;}if (!SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, newInfo, patchInfoLockFile)) {TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, rewrite patch info failed");manager.getPatchReporter().onPatchInfoCorrupted(patchFile, newInfo.oldVersion, newInfo.newVersion);return false;}// Reset patch apply retry count to let us be able to reapply without triggering// patch apply disable when we apply it successfully previously.UpgradePatchRetry.getInstance(context).onPatchResetMaxCheck(patchMd5);TinkerLog.w(TAG, "UpgradePatch tryPatch: done, it is ok");return true;
}

1、复制补丁文件到程序的私有区域使用SharePatchFileUtil.copyFileUsingStream(patchFile, destPatchFile);将补丁文件拷贝到app的私有区域(/data/data目录下)
从/storage/emulated/0/patch_signed_7zip.apk 拷贝到 /data/data/tinker.sample.android/tinker/patch-8b692fe2/patch-8b692fe2.apk。8b692fe2是补丁文件的MD5。
2、合成dex
然后调用DexDiffPatchInternal.tryRecoverDexFiles(),源码如下:

protected static boolean tryRecoverDexFiles(Tinker manager, ShareSecurityCheck checker, Context context,String patchVersionDirectory, File patchFile) {......long begin = SystemClock.elapsedRealtime();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;
}

实际调用patchDexExtractViaDexDiff()方法,源码如下:

private static boolean patchDexExtractViaDexDiff(Context context, String patchVersionDirectory, String meta, final File patchFile) {String dir = patchVersionDirectory + "/" + DEX_PATH + "/";if (!extractDexDiffInternals(context, dir, meta, patchFile, TYPE_DEX)) {TinkerLog.w(TAG, "patch recover, extractDiffInternals fail");return false;}File dexFiles = new File(dir);File[] files = dexFiles.listFiles();List<File> legalFiles = new ArrayList<>();if (files != null) {for (File file : files) {final String fileName = file.getName();// may have directory in android oif (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);final String optimizeDexDirectory = patchVersionDirectory + "/" + DEX_OPTIMIZE_PATH + "/";return dexOptimizeDexFiles(context, legalFiles, optimizeDexDirectory, patchFile);}

调用extractDexDiffInternals(context, dir, meta, patchFile, TYPE_DEX),源码如下:

private static boolean extractDexDiffInternals(Context context, String dir, String meta, File patchFile, int type) {//parsepatchList.clear();ShareDexDiffPatchInfo.parseDexDiffPatchInfo(meta, patchList);if (patchList.isEmpty()) {TinkerLog.w(TAG, "extract patch list is empty! type:%s:", ShareTinkerInternals.getTypeString(type));return true;}File directory = new File(dir);if (!directory.exists()) {directory.mkdirs();}//I think it is better to extract the raw files from apkTinker 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;}// 获取到基准包apk的路径(即/data/app/tinker.sample.android-2/base.apk)String apkPath = applicationInfo.sourceDir;// 基准包文件apk = new ZipFile(apkPath);// 补丁包文件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;}// 遍历 ShareDexDiffPatchInfo,即dex_meta.txt中内容for (ShareDexDiffPatchInfo info : patchList) {long start = System.currentTimeMillis();// 补丁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 虚拟机环境,但是补丁dex是art环境的,就跳过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);//check file whether already exist  如果合成的dex文件已经存在了if (extractedFile.exists()) {// 就校验合成的 dex 文件md5值,如果通过就跳过if (SharePatchFileUtil.verifyDexFileMd5(extractedFile, extractedFileMd5)) {//it is ok, just continueTinkerLog.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 中的记录,进行一个个 dex 文件合成了。// 一开头会去校验合成的文件是否存在,存在的话就跳过,进行下一个。ZipEntry patchFileEntry = patch.getEntry(patchRealPath);ZipEntry rawApkFileEntry = apk.getEntry(patchRealPath);// 如果 oldDexCrc 为0,就说明基准包中对应的 oldDex 文件不存在,直接按照 patch 信息重新打包 dex 即可。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 fileif (!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;}} // // 如果 dexDiffMd5 为 0, 就说明补丁包中没有这个dex,但是基准包中存在else if (dexDiffMd5.equals("0")) {// skip process old dex for real dalvik vm// 如果是 dvm 环境的无须做处理,如果是 art 环境就执行后面,把 oldDex 复制过去if (!isVmArt) {continue;}// 检查基准包中的 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// 检查基准包中的 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);// 直接复制extractDexFile(apk, rawApkFileEntry, extractedFile, info);// 复制完后校验一下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;}} else {// 检查补丁包中 dex 是否存在if (patchFileEntry == null) {TinkerLog.w(TAG, "patch entry is null. path:" + patchRealPath);manager.getPatchReporter().onPatchTypeExtractFail(patchFile, extractedFile, info.rawName, type);return false;}// 检查补丁包中的 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;}// 检查基准包中的 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// 检查基准包中的 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;}// 执行合成操作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;
}

ShareDexDiffPatchInfo.parseDexDiffPatchInfo(meta, patchList);是解析 dex_meta.txt 中的信息,用“,”分割,保存到 patchList 中。那这个dex_meta.txt 从哪里来的?先把补丁文件解压,得到如下:
在这里插入图片描述
进入assets目录,如下:
在这里插入图片描述
dex_meta.txt文件位于补丁文件的assets目录下。dex_meta.txt文件内容如下:

classes.dex,,954e96eeeb5273c2a2ca6d1d2508e265,954e96eeeb5273c2a2ca6d1d2508e265,6395c4636e7cc8f88691dd32a0b60ce5,3282091180,2217937676,jar
test.dex,,56900442eb5b7e1de45449d0685e6e00,56900442eb5b7e1de45449d0685e6e00,0,0,0,jar

dex_meta.txt 记录着:

  • name :补丁 dex 名字
  • path :补丁 dex 路径
  • destMd5InDvm :合成新 dex 在 dvm 中的 md5 值
  • destMd5InArt :合成新 dex 在 art 中的 md5 值
  • dexDiffMd5 :补丁包 dex 文件的 md5 值
  • oldDexCrc :基准包中对应 dex 的 crc 值
  • newDexCrc :合成新 dex 的 crc 值
  • dexMode :dex 类型,为 jar 类型

然后执行一系列检查,具体看代码注释。

上面代码最后一个else情况是:基准包和补丁包中都存在对应 dex 的情况了。代码一开始就是一堆的各种校验,都通过后,调用 patchDexFile 执行合成操作。合成完后再对合成的 dex 进行md5校验。
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 {// 基准包 dex 文件输入流oldDexStream = new BufferedInputStream(baseApk.getInputStream(oldDexEntry));// 补丁包 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 {IOHelper.closeQuietly(zis);}} else {new DexPatchApplier(oldDexStream, patchFileStream).executeAndSaveTo(zos);}zos.closeEntry();} finally {IOHelper.closeQuietly(zos);}} else {new DexPatchApplier(oldDexStream, patchFileStream).executeAndSaveTo(patchedDexFile);}} finally {IOHelper.closeQuietly(oldDexStream);IOHelper.closeQuietly(patchFileStream);}
}

在 patchDexFile() 中,拿到基准包 dex 文件的 InputStream 和补丁包 dex 文件的 InputStream ,然后利用 DexPatchApplier( new DexPatchApplier(oldDexStream, patchFileStream).executeAndSaveTo(zos);) 把这两个流合成一个 dex 文件。
executeAndSaveTo()源码如下:

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 里面合流操作的代码是需要根据 Tinker 的 DexDiff 算法来的。大致就是把两个 Dex 文件的每个分区做 merge 操作。Tinker Dexdiff算法见下面文章《Tinker Dexdiff算法解析》
合并后dex的目录是/data/data/tinker.sample.android/tinker/patch-909c0ac6/dex/tinker_classN.apk。

二、加载合成的dex

加载的代码实际上在生成的Application中调用的,其父类为TinkerApplication,在其attachBaseContext中辗转会调用到loadTinker()方法,在该方法内部,反射调用了TinkerLoader的tryLoad方法。
生成的Application源码如下:

public class SampleApplication extends TinkerApplication {public SampleApplication() {super(15, "tinker.sample.android.app.SampleApplicationLike", "com.tencent.tinker.loader.TinkerLoader", false);}}

第3个参数"com.tencent.tinker.loader.TinkerLoader"就是下面反射中的loaderClassName。
TinkerApplication的onBaseContextAttached()方法源码如下:

private void onBaseContextAttached(Context base) {......loadTinker();......
}

loadTinker()源码如下:

private void loadTinker() {try {//reflect tinker loader, because loaderClass may be define by user!Class<?> tinkerLoadClass = Class.forName(loaderClassName, false, TinkerApplication.class.getClassLoader());Method loadMethod = tinkerLoadClass.getMethod(TINKER_LOADER_METHOD, TinkerApplication.class);Constructor<?> constructor = tinkerLoadClass.getConstructor();tinkerResultIntent = (Intent) loadMethod.invoke(constructor.newInstance(), this);} catch (Throwable e) {//has exception, put exception error codetinkerResultIntent = new Intent();ShareIntentUtil.setIntentReturnCode(tinkerResultIntent, ShareConstants.ERROR_LOAD_PATCH_UNKNOWN_EXCEPTION);tinkerResultIntent.putExtra(INTENT_PATCH_EXCEPTION, e);}
}

这里就是通过反射调用TinkerLoader的tryLoad方法。tryLoad()源码如下:

public Intent tryLoad(TinkerApplication app) {Log.d(TAG, "tryLoad test test");Intent resultIntent = new Intent();long begin = SystemClock.elapsedRealtime();tryLoadPatchFilesInternal(app, resultIntent);long cost = SystemClock.elapsedRealtime() - begin;ShareIntentUtil.setIntentPatchCostTime(resultIntent, cost);return resultIntent;
}

最终调用tryLoadPatchFilesInternal(),源码如下:

private void tryLoadPatchFilesInternal(TinkerApplication app, int tinkerFlag, boolean tinkerLoadVerifyFlag, Intent resultIntent) {// 省略校验代码if (isEnabledForDex) {//tinker/patch.info/patch-641e634c/dexboolean dexCheck = TinkerDexLoader.checkComplete(patchVersionDirectory, securityCheck, resultIntent);if (!dexCheck) {//file not found, do not load patchLog.w(TAG, "tryLoadPatchFiles:dex check fail");return;}}//now we can load patch jarif (isEnabledForDex) {boolean loadTinkerJars = TinkerDexLoader.loadTinkerJars(app, tinkerLoadVerifyFlag, patchVersionDirectory, resultIntent, isSystemOTA);if (!loadTinkerJars) {Log.w(TAG, "tryLoadPatchFiles:onPatchLoadDexesFail");return;}}
}

TinkerDexLoader.checkComplete主要是用于检查下发的meta文件中记录的dex信息(meta文件,可以查看生成patch的产物,在assets/dex-meta.txt),检查meta文件中记录的dex文件信息对应的dex文件是否存在,并把值存在TinkerDexLoader的静态变量dexList中。
然后调用TinkerDexLoader.loadTinkerJars(),源码如下:

@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public static boolean loadTinkerJars(Application application, boolean tinkerLoadVerifyFlag, String directory, Intent intentResult, boolean isSystemOTA) {PathClassLoader classLoader = (PathClassLoader) TinkerDexLoader.class.getClassLoader();String dexPath = directory + "/" + DEX_PATH + "/";File optimizeDir = new File(directory + "/" + DEX_OPTIMIZE_PATH);ArrayList<File> legalFiles = new ArrayList<>();final boolean isArtPlatForm = ShareTinkerInternals.isVmArt();for (ShareDexDiffPatchInfo info : dexList) {//for dalvik, ignore art support dexif (isJustArtSupportDex(info)) {continue;}String path = dexPath + info.realName;File file = new File(path);legalFiles.add(file);}// just for artif (isSystemOTA) {parallelOTAResult = true;parallelOTAThrowable = null;Log.w(TAG, "systemOTA, try parallel oat dexes!!!!!");TinkerParallelDexOptimizer.optimizeAll(legalFiles, optimizeDir,new TinkerParallelDexOptimizer.ResultCallback() {});SystemClassLoaderAdder.installDexes(application, classLoader, optimizeDir, legalFiles);return true;}

找出仅支持art的dex,且当前patch是否仅适用于art时,并行去loadDex。然后调用installDexes(),源码如下:

@SuppressLint("NewApi")
public static void installDexes(Application application, BaseDexClassLoader loader, File dexOptDir, List<File> files, boolean isProtectedApp)throws Throwable {Log.i(TAG, "installDexes dexOptDir: " + dexOptDir.getAbsolutePath() + ", dex size:" + files.size());if (!files.isEmpty()) {files = createSortedAdditionalPathEntries(files);ClassLoader classLoader = loader;if (Build.VERSION.SDK_INT >= 24 && !isProtectedApp) {classLoader = NewClassLoaderInjector.inject(application, loader);}//because in dalvik, if inner class is not the same classloader with it wrapper class.//it won't fail at dex2optif (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);}//install donesPatchDexCount = files.size();Log.i(TAG, "after loaded classloader: " + classLoader + ", dex size:" + sPatchDexCount);if (!checkDexInstall(classLoader)) {//reset patch dexSystemClassLoaderAdder.uninstallPatchDex(classLoader);throw new TinkerRuntimeException(ShareConstants.CHECK_DEX_INSTALL_FAIL);}}
}

V19的install()源码如下:

private static void install(ClassLoader loader, List<File> additionalClassPathEntries,File optimizedDirectory)throws IllegalArgumentException, IllegalAccessException,NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {/* The patched class loader is expected to be a descendant of* dalvik.system.BaseDexClassLoader. We modify its* dalvik.system.DexPathList pathList field to append additional DEX* file entries.*/Field pathListField = ShareReflectUtil.findField(loader, "pathList");Object dexPathList = pathListField.get(loader);ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,suppressedExceptions));if (suppressedExceptions.size() > 0) {for (IOException e : suppressedExceptions) {Log.w(TAG, "Exception in makeDexElement", e);throw e;}}
}
  • 找到PathClassLoader(BaseDexClassLoader)对象中的pathList对象
  • 根据pathList对象找到其中的makeDexElements方法,传入patch相关的对应的
  • 实参,返回Element[]对象
  • 拿到pathList对象中原本的dexElements方法
  • 步骤2与步骤3中的Element[]数组进行合并,将patch相关的dex放在数组的前面
  • 最后将合并后的数组,设置给pathList

这里其实和Qzone的提出的方案基本是一致的。Qzone的方案见《安卓App热补丁动态修复技术介绍 》

这篇关于Android热修复技术-Tinker源码分析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1063110

相关文章

C++ Sort函数使用场景分析

《C++Sort函数使用场景分析》sort函数是algorithm库下的一个函数,sort函数是不稳定的,即大小相同的元素在排序后相对顺序可能发生改变,如果某些场景需要保持相同元素间的相对顺序,可使... 目录C++ Sort函数详解一、sort函数调用的两种方式二、sort函数使用场景三、sort函数排序

在Android平台上实现消息推送功能

《在Android平台上实现消息推送功能》随着移动互联网应用的飞速发展,消息推送已成为移动应用中不可或缺的功能,在Android平台上,实现消息推送涉及到服务端的消息发送、客户端的消息接收、通知渠道(... 目录一、项目概述二、相关知识介绍2.1 消息推送的基本原理2.2 Firebase Cloud Me

Java调用C++动态库超详细步骤讲解(附源码)

《Java调用C++动态库超详细步骤讲解(附源码)》C语言因其高效和接近硬件的特性,时常会被用在性能要求较高或者需要直接操作硬件的场合,:本文主要介绍Java调用C++动态库的相关资料,文中通过代... 目录一、直接调用C++库第一步:动态库生成(vs2017+qt5.12.10)第二步:Java调用C++

kotlin中const 和val的区别及使用场景分析

《kotlin中const和val的区别及使用场景分析》在Kotlin中,const和val都是用来声明常量的,但它们的使用场景和功能有所不同,下面给大家介绍kotlin中const和val的区别,... 目录kotlin中const 和val的区别1. val:2. const:二 代码示例1 Java

SpringBoot3实现Gzip压缩优化的技术指南

《SpringBoot3实现Gzip压缩优化的技术指南》随着Web应用的用户量和数据量增加,网络带宽和页面加载速度逐渐成为瓶颈,为了减少数据传输量,提高用户体验,我们可以使用Gzip压缩HTTP响应,... 目录1、简述2、配置2.1 添加依赖2.2 配置 Gzip 压缩3、服务端应用4、前端应用4.1 N

Go标准库常见错误分析和解决办法

《Go标准库常见错误分析和解决办法》Go语言的标准库为开发者提供了丰富且高效的工具,涵盖了从网络编程到文件操作等各个方面,然而,标准库虽好,使用不当却可能适得其反,正所谓工欲善其事,必先利其器,本文将... 目录1. 使用了错误的time.Duration2. time.After导致的内存泄漏3. jsO

Android中Dialog的使用详解

《Android中Dialog的使用详解》Dialog(对话框)是Android中常用的UI组件,用于临时显示重要信息或获取用户输入,本文给大家介绍Android中Dialog的使用,感兴趣的朋友一起... 目录android中Dialog的使用详解1. 基本Dialog类型1.1 AlertDialog(

Java利用JSONPath操作JSON数据的技术指南

《Java利用JSONPath操作JSON数据的技术指南》JSONPath是一种强大的工具,用于查询和操作JSON数据,类似于SQL的语法,它为处理复杂的JSON数据结构提供了简单且高效... 目录1、简述2、什么是 jsONPath?3、Java 示例3.1 基本查询3.2 过滤查询3.3 递归搜索3.4

Python中随机休眠技术原理与应用详解

《Python中随机休眠技术原理与应用详解》在编程中,让程序暂停执行特定时间是常见需求,当需要引入不确定性时,随机休眠就成为关键技巧,下面我们就来看看Python中随机休眠技术的具体实现与应用吧... 目录引言一、实现原理与基础方法1.1 核心函数解析1.2 基础实现模板1.3 整数版实现二、典型应用场景2

Python实现无痛修改第三方库源码的方法详解

《Python实现无痛修改第三方库源码的方法详解》很多时候,我们下载的第三方库是不会有需求不满足的情况,但也有极少的情况,第三方库没有兼顾到需求,本文将介绍几个修改源码的操作,大家可以根据需求进行选择... 目录需求不符合模拟示例 1. 修改源文件2. 继承修改3. 猴子补丁4. 追踪局部变量需求不符合很