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

相关文章

怎样通过分析GC日志来定位Java进程的内存问题

《怎样通过分析GC日志来定位Java进程的内存问题》:本文主要介绍怎样通过分析GC日志来定位Java进程的内存问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、GC 日志基础配置1. 启用详细 GC 日志2. 不同收集器的日志格式二、关键指标与分析维度1.

MySQL中的表连接原理分析

《MySQL中的表连接原理分析》:本文主要介绍MySQL中的表连接原理分析,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1、背景2、环境3、表连接原理【1】驱动表和被驱动表【2】内连接【3】外连接【4编程】嵌套循环连接【5】join buffer4、总结1、背景

python中Hash使用场景分析

《python中Hash使用场景分析》Python的hash()函数用于获取对象哈希值,常用于字典和集合,不可变类型可哈希,可变类型不可,常见算法包括除法、乘法、平方取中和随机数哈希,各有优缺点,需根... 目录python中的 Hash除法哈希算法乘法哈希算法平方取中法随机数哈希算法小结在Python中,

Java Stream的distinct去重原理分析

《JavaStream的distinct去重原理分析》Javastream中的distinct方法用于去除流中的重复元素,它返回一个包含过滤后唯一元素的新流,该方法会根据元素的hashcode和eq... 目录一、distinct 的基础用法与核心特性二、distinct 的底层实现原理1. 顺序流中的去重

Qt如何实现文本编辑器光标高亮技术

《Qt如何实现文本编辑器光标高亮技术》这篇文章主要为大家详细介绍了Qt如何实现文本编辑器光标高亮技术,文中的示例代码讲解详细,具有一定的借鉴价值,有需要的小伙伴可以了解下... 目录实现代码函数作用概述代码详解 + 注释使用 QTextEdit 的高亮技术(重点)总结用到的关键技术点应用场景举例示例优化建议

Android DataBinding 与 MVVM使用详解

《AndroidDataBinding与MVVM使用详解》本文介绍AndroidDataBinding库,其通过绑定UI组件与数据源实现自动更新,支持双向绑定和逻辑运算,减少模板代码,结合MV... 目录一、DataBinding 核心概念二、配置与基础使用1. 启用 DataBinding 2. 基础布局

Android ViewBinding使用流程

《AndroidViewBinding使用流程》AndroidViewBinding是Jetpack组件,替代findViewById,提供类型安全、空安全和编译时检查,代码简洁且性能优化,相比Da... 目录一、核心概念二、ViewBinding优点三、使用流程1. 启用 ViewBinding (模块级

关于MyISAM和InnoDB对比分析

《关于MyISAM和InnoDB对比分析》:本文主要介绍关于MyISAM和InnoDB对比分析,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录开篇:从交通规则看存储引擎选择理解存储引擎的基本概念技术原理对比1. 事务支持:ACID的守护者2. 锁机制:并发控制的艺

qt5cored.dll报错怎么解决? 电脑qt5cored.dll文件丢失修复技巧

《qt5cored.dll报错怎么解决?电脑qt5cored.dll文件丢失修复技巧》在进行软件安装或运行程序时,有时会遇到由于找不到qt5core.dll,无法继续执行代码,这个问题可能是由于该文... 遇到qt5cored.dll文件错误时,可能会导致基于 Qt 开发的应用程序无法正常运行或启动。这种错

电脑提示xlstat4.dll丢失怎么修复? xlstat4.dll文件丢失处理办法

《电脑提示xlstat4.dll丢失怎么修复?xlstat4.dll文件丢失处理办法》长时间使用电脑,大家多少都会遇到类似dll文件丢失的情况,不过,解决这一问题其实并不复杂,下面我们就来看看xls... 在Windows操作系统中,xlstat4.dll是一个重要的动态链接库文件,通常用于支持各种应用程序