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中注册的代码如下:

<service
    android: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 InnerService
            startService(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 file
    File destPatchFile = new File(patchVersionDirectory + "/" + SharePatchFileUtil.getPatchVersionFile(patchMd5));

    try {
        // check md5 first
        if (!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、合成dex
    if (!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 interpreted
    if (!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 o
            if (file.isFile()
                &&  (fileName.endsWith(ShareConstants.DEX_SUFFIX)
                  || fileName.endsWith(ShareConstants.JAR_SUFFIX)
                  || fileName.endsWith(ShareConstants.PATCH_SUFFIX))
            ) {
                legalFiles.add(file);
            }
        }
    }

    TinkerLog.i(TAG, "legal files to do dexopt: " + legalFiles);

    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) {
    //parse
    patchList.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 apk
    Tinker manager = Tinker.with(context);
    ZipFile apk = null;
    ZipFile patch = null;
    try {
        ApplicationInfo applicationInfo = context.getApplicationInfo();
        if (applicationInfo == null) {
            // Looks like running on a test Context, so just return without patching.
            TinkerLog.w(TAG, "applicationInfo == null!!!!");
            return false;
        }
		// 获取到基准包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 continue
                    TinkerLog.w(TAG, "dex file %s is already exist, and md5 match, just continue", extractedFile.getPath());
                    continue;
                } else {
                    TinkerLog.w(TAG, "have a mismatch corrupted dex " + extractedFile.getPath());
                    // 否则删除文件
                    extractedFile.delete();
                }
            } else {
                extractedFile.getParentFile().mkdirs();
            }
			// 从这里开始,就是遍历 patchList 中的记录,进行一个个 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 file
                if (!extractDexFile(patch, patchFileEntry, extractedFile, info)) {
                    TinkerLog.w(TAG, "Failed to extract raw patch file " + extractedFile.getPath());
                    manager.getPatchReporter().onPatchTypeExtractFail(patchFile, extractedFile, info.rawName, type);
                    return false;
                }
            } 
            // // 如果 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 code
        tinkerResultIntent = 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/dex
        boolean dexCheck = TinkerDexLoader.checkComplete(patchVersionDirectory, securityCheck, resultIntent);
        if (!dexCheck) {
            //file not found, do not load patch
            Log.w(TAG, "tryLoadPatchFiles:dex check fail");
            return;
        }
    }

    //now we can load patch jar
    if (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 dex
            if (isJustArtSupportDex(info)) {
                continue;
            }
            String path = dexPath + info.realName;
            File file = new File(path);

            legalFiles.add(file);
        }
        // just for art
        if (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 dex2opt
        if (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 done
        sPatchDexCount = files.size();
        Log.i(TAG, "after loaded classloader: " + classLoader + ", dex size:" + sPatchDexCount);

        if (!checkDexInstall(classLoader)) {
            //reset patch dex
            SystemClassLoaderAdder.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热补丁动态修复技术介绍 》

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值