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

相关文章

性能分析之MySQL索引实战案例

文章目录 一、前言二、准备三、MySQL索引优化四、MySQL 索引知识回顾五、总结 一、前言 在上一讲性能工具之 JProfiler 简单登录案例分析实战中已经发现SQL没有建立索引问题,本文将一起从代码层去分析为什么没有建立索引? 开源ERP项目地址:https://gitee.com/jishenghua/JSH_ERP 二、准备 打开IDEA找到登录请求资源路径位置

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

【专题】2024飞行汽车技术全景报告合集PDF分享(附原数据表)

原文链接: https://tecdat.cn/?p=37628 6月16日,小鹏汇天旅航者X2在北京大兴国际机场临空经济区完成首飞,这也是小鹏汇天的产品在京津冀地区进行的首次飞行。小鹏汇天方面还表示,公司准备量产,并计划今年四季度开启预售小鹏汇天分体式飞行汽车,探索分体式飞行汽车城际通勤。阅读原文,获取专题报告合集全文,解锁文末271份飞行汽车相关行业研究报告。 据悉,业内人士对飞行汽车行业

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

Android平台播放RTSP流的几种方案探究(VLC VS ExoPlayer VS SmartPlayer)

技术背景 好多开发者需要遴选Android平台RTSP直播播放器的时候,不知道如何选的好,本文针对常用的方案,做个大概的说明: 1. 使用VLC for Android VLC Media Player(VLC多媒体播放器),最初命名为VideoLAN客户端,是VideoLAN品牌产品,是VideoLAN计划的多媒体播放器。它支持众多音频与视频解码器及文件格式,并支持DVD影音光盘,VCD影

金融业开源技术 术语

金融业开源技术  术语 1  范围 本文件界定了金融业开源技术的常用术语。 本文件适用于金融业中涉及开源技术的相关标准及规范性文件制定和信息沟通等活动。

Java ArrayList扩容机制 (源码解读)

结论:初始长度为10,若所需长度小于1.5倍原长度,则按照1.5倍扩容。若不够用则按照所需长度扩容。 一. 明确类内部重要变量含义         1:数组默认长度         2:这是一个共享的空数组实例,用于明确创建长度为0时的ArrayList ,比如通过 new ArrayList<>(0),ArrayList 内部的数组 elementData 会指向这个 EMPTY_EL

如何在Visual Studio中调试.NET源码

今天偶然在看别人代码时,发现在他的代码里使用了Any判断List<T>是否为空。 我一般的做法是先判断是否为null,再判断Count。 看了一下Count的源码如下: 1 [__DynamicallyInvokable]2 public int Count3 {4 [__DynamicallyInvokable]5 get

SWAP作物生长模型安装教程、数据制备、敏感性分析、气候变化影响、R模型敏感性分析与贝叶斯优化、Fortran源代码分析、气候数据降尺度与变化影响分析

查看原文>>>全流程SWAP农业模型数据制备、敏感性分析及气候变化影响实践技术应用 SWAP模型是由荷兰瓦赫宁根大学开发的先进农作物模型,它综合考虑了土壤-水分-大气以及植被间的相互作用;是一种描述作物生长过程的一种机理性作物生长模型。它不但运用Richard方程,使其能够精确的模拟土壤中水分的运动,而且耦合了WOFOST作物模型使作物的生长描述更为科学。 本文让更多的科研人员和农业工作者

MOLE 2.5 分析分子通道和孔隙

软件介绍 生物大分子通道和孔隙在生物学中发挥着重要作用,例如在分子识别和酶底物特异性方面。 我们介绍了一种名为 MOLE 2.5 的高级软件工具,该工具旨在分析分子通道和孔隙。 与其他可用软件工具的基准测试表明,MOLE 2.5 相比更快、更强大、功能更丰富。作为一项新功能,MOLE 2.5 可以估算已识别通道的物理化学性质。 软件下载 https://pan.quark.cn/s/57