Android 热修复Tinker源码分析(二)补丁包的合成

2,925 阅读12分钟

前言

对于重要类的分析已经全部加上了详细注释,代码在github.com/ColorfulHor…

上一篇文章我们分析了补丁包的生成,本文从源码开始简单分析补丁包的合成过程,限于篇幅重点分析Dex合成过程,涉及到的知识包括dex加载机制,Art编译机制等。

Tinker一个基线包可以打多个补丁,因为每一个补丁包都是在基线包上patch出来的,合成后的产物会保留在单独的目录,互不影响,也不影响原始app,可以随时清除回到基线版本。

初始化Tinker

想要使用Tinker提供的API需要先调用tinker.install进行初始化,否则只能使用TinkerApplicationHelper中的api,初始化tinker支持一些自定义配置,主要是一些回调注册,比如加载补丁回调,合成补丁回调等,可选参数如下

  • applicationLike application代理
  • loadReporter 加载补丁的回调类,默认DefaultLoadReporter
  • patchReporter 合成补丁的回调类,默认DefaultPatchReporter
  • listener 接收合成补丁任务的类,默认DefaultPatchListener
  • resultServiceClass patch补丁合成进程将合成结果返回给主进程的类,默认DefaultTinkerResultService
  • upgradePatchProcessor 执行补丁合成操作的类,默认UpgradePatch
public static Tinker install(ApplicationLike applicationLike, LoadReporter loadReporter, PatchReporter patchReporter,
                             PatchListener listener, Class<? extends AbstractResultService> resultServiceClass,
                             AbstractPatch upgradePatchProcessor) {
    // 创建实例,注册一些回调
    Tinker tinker = new Tinker.Builder(applicationLike.getApplication())
        .tinkerFlags(applicationLike.getTinkerFlags())
        .loadReport(loadReporter)
        .listener(listener)
        .patchReporter(patchReporter)
        // 加载补丁是否校验md5
        .tinkerLoadVerifyFlag(applicationLike.getTinkerLoadVerifyFlag()).build();
    Tinker.create(tinker);
    // getTinkerResultIntent得到的是此次启动加载补丁的结果信息
    tinker.install(applicationLike.getTinkerResultIntent(), resultServiceClass, upgradePatchProcessor);
    return tinker;
}
public void install(Intent intentResult, Class<? extends AbstractResultService> serviceClass,
                        AbstractPatch upgradePatch) {
    sInstalled = true;
    // patch service设置用于补丁合成的UpgradePatch实例,以及合成结果回调类DefaultTinkerResultService
    TinkerPatchService.setPatchProcessor(upgradePatch, serviceClass);
    ......
    tinkerLoadResult = new TinkerLoadResult();
    tinkerLoadResult.parseTinkerResult(getContext(), intentResult);
    // 回调补丁加载结果
    loadReporter.onLoadResult(patchDirectory, tinkerLoadResult.loadCode, tinkerLoadResult.costTime);
}

补丁合成

下载补丁成功后通过调用TinkerInstaller.onReceiveUpgradePatch合成补丁,然后直接调用到DefaultPatchListener.onPatchReceived方法,此方法中首先调用patchCheck检查补丁是否应该被合成,然后启动TinkerPatchService合成补丁。

public class DefaultPatchListener implements PatchListener {
    public int onPatchReceived(String path) {
        final File patchFile = new File(path);
        // 差异apk md5
        final String patchMD5 = SharePatchFileUtil.getMD5(patchFile);
        // 校验补丁
        // 检查是否应该合成该补丁
        // 补丁不合法/正在合成补丁/系统OTA后第一次启动
        //该版本补丁已经被加载/该版本补丁已经合成还未加载/该补丁合并失败超过阈值
        final int returnCode = patchCheck(path, patchMD5);
        if (returnCode == ShareConstants.ERROR_PATCH_OK) {
            // 绑定TinkerPatchForeService,它运行在:patch进程
            runForgService();
            // 启动TinkerPatchService合成补丁
            TinkerPatchService.runPatchService(context, path);
        } else {
            Tinker.with(context).getLoadReporter().onLoadPatchListenerReceiveFail(new File(path), returnCode);
        }
        return returnCode;
    }
}

是否需要合成补丁

patchCheck主要检查补丁包合法性以及该版本补丁是否已经加载/合成过,以此判断是否需要合成该补丁,要注意的是在系统进行OTA升级后第一次启动也不能合成补丁,因为此时是以解释模式运行,需要重新进行dexopt。这里贴一下校验过程注释代码。 关于interpretMode(解释模式)的相关逻辑用于加载补丁,会单独在加载补丁中分析,这里不用太关注。

protected int patchCheck(String path, String patchMd5) {
    final Tinker manager = Tinker.with(context);
    // 是否启用了tinker
    if (!manager.isTinkerEnabled() || !ShareTinkerInternals.isTinkerEnableWithSharedPreferences(context)) {
        return ShareConstants.ERROR_PATCH_DISABLE;
    }
    // md5以及文件合法性校验
    if (TextUtils.isEmpty(patchMd5)) {
        return ShareConstants.ERROR_PATCH_NOTEXIST;
    }
    final File file = new File(path);
    if (!SharePatchFileUtil.isLegalFile(file)) {
        return ShareConstants.ERROR_PATCH_NOTEXIST;
    }
    // 不能在patch进程调用
    if (manager.isPatchProcess()) {
        return ShareConstants.ERROR_PATCH_INSERVICE;
    }
    // patch进程已经在运行则忽略
    if (TinkerServiceInternals.isTinkerPatchServiceRunning(context)) {
        return ShareConstants.ERROR_PATCH_RUNNING;
    }
    // 这里判断是否在7.0以下系统错误地开启了jit选项(7.0以上Art才重新引入jit,某些自定义rom会错误打开此选项)
    if (ShareTinkerInternals.isVmJit()) {
        return ShareConstants.ERROR_PATCH_JIT;
    }
    // 此次启动加载补丁的信息
    final TinkerLoadResult loadResult = manager.getTinkerLoadResultIfPresent();
    // 当前是否在以解释模式运行,true则说明需要重新做dexopt
    final boolean repairOptNeeded = manager.isMainProcess()
            && loadResult != null && loadResult.useInterpretMode;
    if (!repairOptNeeded) {
        if (manager.isTinkerLoaded() && loadResult != null) {
            String currentVersion = loadResult.currentVersion;
            // 该版本的补丁已经被加载则忽略
            if (patchMd5.equals(currentVersion)) {
                return ShareConstants.ERROR_PATCH_ALREADY_APPLY;
            }
        }
        // 该补丁已经合成,但是主进程没有重启加载,这里也忽略,不再重复合成
        final String patchDirectory = manager.getPatchDirectory().getAbsolutePath();
        File patchInfoLockFile = SharePatchFileUtil.getPatchInfoLockFile(patchDirectory);
        File patchInfoFile = SharePatchFileUtil.getPatchInfoFile(patchDirectory);
        try {
            final SharePatchInfo currInfo = SharePatchInfo.readAndCheckPropertyWithLock(patchInfoFile, patchInfoLockFile);
            if (currInfo != null && !ShareTinkerInternals.isNullOrNil(currInfo.newVersion) && !currInfo.isRemoveNewVersion) {
                if (patchMd5.equals(currInfo.newVersion)) {
                    return ShareConstants.ERROR_PATCH_ALREADY_APPLY;
                }
            }
        } catch (Throwable ignored) {
            // Ignored.
        }
    }
    // 合成补丁重试次数超过阈值(20次)忽略
    if (!UpgradePatchRetry.getInstance(context).onPatchListenerCheck(patchMd5)) {
        return ShareConstants.ERROR_PATCH_RETRY_COUNT_LIMIT;
    }
    return ShareConstants.ERROR_PATCH_OK;
}

TinkerPatchService合成补丁预操作

TinkerPatchService是一个IntentService,运行在:patch合成进程,主要做一下补丁合成的预操作,主要逻辑在doApplyPatch方法中。

  • 调用DefaultPatchReporter.onPatchServiceStart回调,记录合成该补丁重试次数
  • 调用UpgradePatch.tryPatch真正开始合成补丁
  • 合成完毕回调结果到DefaultTinkerResultService,删除下载的补丁源文件,杀死进程重启
public class TinkerPatchService extends IntentService {
    ......
    public static void runPatchService(final Context context, final String path) {
        ShareTinkerLog.i(TAG, "run patch service...");
        Intent intent = new Intent(context, TinkerPatchService.class);
        // 补丁路径
        intent.putExtra(PATCH_PATH_EXTRA, path);
        // 合成结果回调类名,默认DefaultTinkerResultService
        intent.putExtra(RESULT_CLASS_EXTRA, resultServiceClass.getName());
        try {
            context.startService(intent);
        } catch (Throwable thr) {
            ShareTinkerLog.e(TAG, "run patch service fail, exception:" + thr);
        }
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        // 设为前台服务提高优先级
        increasingPriority();
        doApplyPatch(this, intent);
    }


    private static AtomicBoolean sIsPatchApplying = new AtomicBoolean(false);

    private static void doApplyPatch(Context context, Intent intent) {
        // Since we may retry with IntentService, we should prevent
        // racing here again.
        if (!sIsPatchApplying.compareAndSet(false, true)) {
            ShareTinkerLog.w(TAG, "TinkerPatchService doApplyPatch is running by another runner.");
            return;
        }

        Tinker tinker = Tinker.with(context);
        // 回调事件
        tinker.getPatchReporter().onPatchServiceStart(intent);

        if (intent == null) {
            ShareTinkerLog.e(TAG, "TinkerPatchService received a null intent, ignoring.");
            return;
        }
        // 补丁文件路径
        String path = getPatchPathExtra(intent);
        if (path == null) {
            ShareTinkerLog.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.");
            }
            // 调用UpgradePatch tryPatch合成补丁
            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;
        // 回调合成结果到DefaultTinkerResultService
        AbstractResultService.runResultService(context, patchResult, getPatchResultExtra(intent));

        sIsPatchApplying.set(false);
    }
}

开始合成补丁

默认UpgradePatch.tryPatch执行补丁合成逻辑,大致步骤如下

  1. ShareSecurityCheck类提取补丁包中xxx_meta文件,并校验签名
  2. ShareTinkerInternals解析并校验补丁的TinkerId、签名、MD5、meta文件等的合法性
  3. 读取解析data/data/包名/tinker/patch.info文件,校验该补丁是否可以被合成;此文件记录当前版本加载补丁的信息,对应类为SharePatchInfo
  4. 将补丁包改名为patch-xxx.apk,拷贝到data/data/包名/tinker/patch-xxx目录
  5. 分别合成补丁中dex文件,资源文件,so库,合成dex后对新dex做dex2oat操作 本文主要对Dex合成流程进行分析,so库以及资源文件合成部分比较简单,只需要关心输出目录即可
public class UpgradePatch extends AbstractPatch {
    private static final String TAG = "Tinker.UpgradePatch";

    @Override
    public boolean tryPatch(Context context, String tempPatchPath, PatchResult patchResult) {
        Tinker manager = Tinker.with(context);
        final File patchFile = new File(tempPatchPath);
        if (!manager.isTinkerEnabled() || !ShareTinkerInternals.isTinkerEnableWithSharedPreferences(context)) {
            return false;
        }

        if (!SharePatchFileUtil.isLegalFile(patchFile)) {
            return false;
        }
        // 此类用于读取补丁包中的meta文件
        ShareSecurityCheck signatureCheck = new ShareSecurityCheck(context);
        // 解析并校验补丁的TinkerId、签名、MD5、meta文件等的合法性
        int returnCode = ShareTinkerInternals.checkTinkerPackage(context, manager.getTinkerFlags(), patchFile, signatureCheck);
        if (returnCode != ShareConstants.ERROR_PACKAGE_CHECK_OK) {
            manager.getPatchReporter().onPatchPackageCheckFail(patchFile, returnCode);
            return false;
        }

        String patchMd5 = SharePatchFileUtil.getMD5(patchFile);
        if (patchMd5 == null) {
            return false;
        }
        // 补丁包md5作为版本号
        patchResult.patchVersion = patchMd5;

        ShareTinkerLog.i(TAG, "UpgradePatch tryPatch:patchMd5:%s", patchMd5);

        // data/data/包名/tinker
        final String patchDirectory = manager.getPatchDirectory().getAbsolutePath();

        File patchInfoLockFile = SharePatchFileUtil.getPatchInfoLockFile(patchDirectory);
        // data/data/包名/tinker/patch.info
        File patchInfoFile = SharePatchFileUtil.getPatchInfoFile(patchDirectory);
        // 读取package_meta.txt内容
        final Map<String, String> pkgProps = signatureCheck.getPackagePropertiesIfPresent();
        if (pkgProps == null) {
            ShareTinkerLog.e(TAG, "UpgradePatch packageProperties is null, do we process a valid patch apk ?");
            return false;
        }

        final String isProtectedAppStr = pkgProps.get(ShareConstants.PKGMETA_KEY_IS_PROTECTED_APP);
        final boolean isProtectedApp = (isProtectedAppStr != null && !isProtectedAppStr.isEmpty() && !"0".equals(isProtectedAppStr));
        // 读取上一次合成补丁的信息
        SharePatchInfo oldInfo = SharePatchInfo.readAndCheckPropertyWithLock(patchInfoFile, patchInfoLockFile);

        SharePatchInfo newInfo;

        if (oldInfo != null) {
            // 已经加载过补丁
            if (oldInfo.oldVersion == null || oldInfo.newVersion == null || oldInfo.oatDir == null) {
                ShareTinkerLog.e(TAG, "UpgradePatch tryPatch:onPatchInfoCorrupted");
                manager.getPatchReporter().onPatchInfoCorrupted(patchFile, oldInfo.oldVersion, oldInfo.newVersion);
                return false;
            }

            if (!SharePatchFileUtil.checkIfMd5Valid(patchMd5)) {
                ShareTinkerLog.e(TAG, "UpgradePatch tryPatch:onPatchVersionCheckFail md5 %s is valid", patchMd5);
                manager.getPatchReporter().onPatchVersionCheckFail(patchFile, oldInfo, patchMd5);
                return false;
            }
            // 当前是否以解释模式加载了补丁(系统OTA之后第一次运行)
            // 在TinkerLoader中以解释模式加载了补丁,oatDir会被设为interpret
            final boolean usingInterpret = oldInfo.oatDir.equals(ShareConstants.INTERPRET_DEX_OPTIMIZE_PATH);
            // 判断该补丁是否已合成过,当前要合成的补丁已经被加载则不合成
            if (!usingInterpret && !ShareTinkerInternals.isNullOrNil(oldInfo.newVersion) && oldInfo.newVersion.equals(patchMd5) && !oldInfo.isRemoveNewVersion) {
                ShareTinkerLog.e(TAG, "patch already applied, md5: %s", patchMd5);

                // 该补丁已经合成成功过,合成重试次数重置为1
                UpgradePatchRetry.getInstance(context).onPatchResetMaxCheck(patchMd5);

                return true;
            }
            // 当前是以解释模式加载补丁时,将oatDir设为changing,以便下次加载补丁时不再以解释模式加载
            final String finalOatDir = usingInterpret ? ShareConstants.CHANING_DEX_OPTIMIZE_PATH : oldInfo.oatDir;
            newInfo = new SharePatchInfo(oldInfo.oldVersion, patchMd5, isProtectedApp, false, Build.FINGERPRINT, finalOatDir, false);
        } else {
            // 没有加载过补丁
            newInfo = new SharePatchInfo("", patchMd5, isProtectedApp, false, Build.FINGERPRINT, ShareConstants.DEFAULT_DEX_OPTIMIZE_PATH, false);
        }

        final String patchName = SharePatchFileUtil.getPatchVersionDirectory(patchMd5);
        // data/data/包名/tinker/patch-xxx
        final String patchVersionDirectory = patchDirectory + "/" + patchName;

        ShareTinkerLog.i(TAG, "UpgradePatch tryPatch:patchVersionDirectory:%s", patchVersionDirectory);

        // data/data/包名/tinker/patch-xxx/patch-xxx.apk
        File destPatchFile = new File(patchVersionDirectory + "/" + SharePatchFileUtil.getPatchVersionFile(patchMd5));

        try {
            if (!patchMd5.equals(SharePatchFileUtil.getMD5(destPatchFile))) {
                // 将补丁包改名拷贝到destPatchFile目录
                SharePatchFileUtil.copyFileUsingStream(patchFile, destPatchFile);
                ShareTinkerLog.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) {
            ShareTinkerLog.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;
        }

        // 合成dex文件,并且进行dex2oat
        if (!DexDiffPatchInternal.tryRecoverDexFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile, patchResult)) {
            ShareTinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch dex failed");
            return false;
        }
        // 方舟编译器相关处理
        if (!ArkHotDiffPatchInternal.tryRecoverArkHotLibrary(manager, signatureCheck,
                context, patchVersionDirectory, destPatchFile)) {
            return false;
        }
        // BSDiff合并so库,验证流程和合并dex差不多,最后通过BSPatch.patchFast合成
        if (!BsDiffPatchInternal.tryRecoverLibraryFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {
            ShareTinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch library failed");
            return false;
        }
        // BSDiff合并资源文件,验证流程和合并dex差不多,最后通过BSPatch.patchFast合成
        // 输出目录data/data/包名/tinker/patch-xxx/res/resources.apk
        if (!ResDiffPatchInternal.tryRecoverResourceFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {
            ShareTinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch resource failed");
            return false;
        }

        // 检查dex2oat产物是否有缺失,因为vivo/oppo会异步执行dex2oat
        if (!DexDiffPatchInternal.waitAndCheckDexOptFile(patchFile, manager)) {
            ShareTinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, check dex opt file failed");
            return false;
        }
        // 将合成补丁信息写入到patch.info文件
        if (!SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, newInfo, patchInfoLockFile)) {
            ShareTinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, rewrite patch info failed");
            manager.getPatchReporter().onPatchInfoCorrupted(patchFile, newInfo.oldVersion, newInfo.newVersion);
            return false;
        }

        // 重置记录的补丁合成次数
        UpgradePatchRetry.getInstance(context).onPatchResetMaxCheck(patchMd5);
        return true;
    }
}

dex预合成

补丁合成最关键的是dex文件合成,我们着重分析这部分逻辑,这里还是列一下简单流程

  1. 入口是DexDiffPatchInternal.tryRecoverDexFiles方法,然后调用到patchDexExtractViaDexDiff
  2. patchDexExtractViaDexDiff方法中extractDexDiffInternals方法对dex做一些预校验,然后开始合成
  3. 最后调用dexOptimizeDexFiles对合成后dex做dexopt操作,产物保存在data/data/包名/tinker/patch-xxx/odex
public class DexDiffPatchInternal extends BasePatchInternal {
    private static boolean patchDexExtractViaDexDiff(Context context, String patchVersionDirectory, String meta, final File patchFile, PatchResult patchResult) {
        // data/data/包名/tinker/patch-xxx/dex
        String dir = patchVersionDirectory + "/" + DEX_PATH + "/";
        // 合成dex
        if (!extractDexDiffInternals(context, dir, meta, patchFile, TYPE_DEX)) {
            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);
                }
            }
        }

        ShareTinkerLog.i(TAG, "legal files to do dexopt: " + legalFiles);
        // 存放dexopt产物,data/data/包名/tinker/patch-xxx/odex
        final String optimizeDexDirectory = patchVersionDirectory + "/" + DEX_OPTIMIZE_PATH + "/";
        // 触发执行dexopt
        return dexOptimizeDexFiles(context, legalFiles, optimizeDexDirectory, patchFile, patchResult);
    }
}

dex合成

DexDiffPatchInternal.extractDexDiffInternals方法对于dex做预校验,具体实现在DexPatchApplier中,大致流程如下

  1. 解析补丁包中dex_meta文件,将变更dex信息解析为ShareDexDiffPatchInfo对象装入patchList
  2. 对于Art平台,checkClassNDexFiles方法检查data/data/包名/tinker/patch-xxx/dex/tinker_classN.apk是否存在(此文件是Art下合成补丁后的所有new dex打包而成的),判断此补丁是否已经加载过;对于Dalvik平台则不打包tinker_classN.apk,但是也要校验每个Dex是否需要合成
  3. 遍历patchList开始依次合成dex,调用patchDexFile最后通过DexPatchApplier.executeAndSaveTo合成dex,内部算法实现是DexDiff,合成后dex保存到data/data/包名/tinker/patch-xxx/dex
  4. 每个dex合成完成后都需要校验md5,保证此dex合成正确性,所有dex合成完毕后,Art下需要将所有dex打包成tinker_classN.apk 代码+详细注释:
public class DexDiffPatchInternal extends BasePatchInternal {
    ......
    private static boolean extractDexDiffInternals(Context context, String dir, String meta, File patchFile, int type) {
        patchList.clear();
        // 解析dex_meta文件,结果装入patchList
        ShareDexDiffPatchInfo.parseDexDiffPatchInfo(meta, patchList);
        if (patchList.isEmpty()) {
            return true;
        }
        // data/data/包名/tinker/patch-xxx/dex
        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.
                ShareTinkerLog.w(TAG, "applicationInfo == null!!!!");
                return false;
            }

            String apkPath = applicationInfo.sourceDir;
            // 原apk
            apk = new ZipFile(apkPath);
            // 补丁包
            patch = new ZipFile(patchFile);
            // art下合成补丁,合成所有old dex和patch dex,然后打包为tinker_classN.apk,dalvik下不打包dex
            // 判断是否需要生成tinker_classN.apk文件
            if (checkClassNDexFiles(dir)) {
                ShareTinkerLog.w(TAG, "class n dex file %s is already exist, and md5 match, just continue", ShareConstants.CLASS_N_APK_NAME);
                return true;
            }
            for (ShareDexDiffPatchInfo info : patchList) {
                long start = System.currentTimeMillis();

                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;
                // 非主dex,且该dex没有改变的话destMd5InDvm字段值为"0",此dex在dalvik下无需合成
                if (!isVmArt && info.destMd5InDvm.equals("0")) {
                    ShareTinkerLog.w(TAG, "patch dex %s is only for art, just continue", patchRealPath);
                    continue;
                }
                String extractedFileMd5 = isVmArt ? info.destMd5InArt : info.destMd5InDvm;

                if (!SharePatchFileUtil.checkIfMd5Valid(extractedFileMd5)) {
                    ShareTinkerLog.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;
                }
                // data/data/包名/tinker/patch-xxx/dex/dex名称,用于存放合成后的dex
                File extractedFile = new File(dir + info.realName);

                // 检查合成后的dex(此时还未合成,如果存在说明之前已经合成过该dex)是否已经存在,存在说明已经合成过该dex
                // 已经存在则要验证是否和补丁包中记录的预合成的dex的md5一致性,不一致需要删除已存在的dex
                if (extractedFile.exists()) {
                    if (SharePatchFileUtil.verifyDexFileMd5(extractedFile, extractedFileMd5)) {
                        //it is ok, just continue
                        ShareTinkerLog.w(TAG, "dex file %s is already exist, and md5 match, just continue", extractedFile.getPath());
                        continue;
                    } else {
                        ShareTinkerLog.w(TAG, "have a mismatch corrupted dex " + extractedFile.getPath());
                        extractedFile.delete();
                    }
                } else {
                    extractedFile.getParentFile().mkdirs();
                }
                // 补丁包中patch dex
                ZipEntry patchFileEntry = patch.getEntry(patchRealPath);
                // old dex
                ZipEntry rawApkFileEntry = apk.getEntry(patchRealPath);
                if (oldDexCrc.equals("0")) {
                    // oldDexCrc为"0"表明该dex是新增的
                    if (patchFileEntry == null) {
                        ShareTinkerLog.w(TAG, "patch entry is null. path:" + patchRealPath);
                        manager.getPatchReporter().onPatchTypeExtractFail(patchFile, extractedFile, info.rawName, type);
                        return false;
                    }

                    // 提取补丁包中dex到data/data/包名/tinker/patch-xxx/dex/
                    if (!extractDexFile(patch, patchFileEntry, extractedFile, info)) {
                        ShareTinkerLog.w(TAG, "Failed to extract raw patch file " + extractedFile.getPath());
                        manager.getPatchReporter().onPatchTypeExtractFail(patchFile, extractedFile, info.rawName, type);
                        return false;
                    }
                } else if (dexDiffMd5.equals("0")) {
                    // oldDexCrc不为"0",dexDiffMd5为"0"代表该dex没有改变
                    // 此情况art下需要将old dex拷贝到补丁dex目录,dalvik下忽略
                    if (!isVmArt) {
                        continue;
                    }

                    if (rawApkFileEntry == null) {
                        ShareTinkerLog.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
                    String rawEntryCrc = String.valueOf(rawApkFileEntry.getCrc());
                    // old dex crc校验(补丁包中记录的old dex crc和当前apk中old dex crc)
                    if (!rawEntryCrc.equals(oldDexCrc)) {
                        ShareTinkerLog.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;
                    }

                    // 从当前apk中将old dex复制到data/data/包名/tinker/patch-xxx/dex/
                    extractDexFile(apk, rawApkFileEntry, extractedFile, info);

                    if (!SharePatchFileUtil.verifyDexFileMd5(extractedFile, extractedFileMd5)) {
                        ShareTinkerLog.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 {
                    // 此分支中old dex patch dex都应该存在
                    if (patchFileEntry == null) {
                        ShareTinkerLog.w(TAG, "patch entry is null. path:" + patchRealPath);
                        manager.getPatchReporter().onPatchTypeExtractFail(patchFile, extractedFile, info.rawName, type);
                        return false;
                    }

                    if (!SharePatchFileUtil.checkIfMd5Valid(dexDiffMd5)) {
                        ShareTinkerLog.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;
                    }
                    // 当前apk中old dex是否存在
                    if (rawApkFileEntry == null) {
                        ShareTinkerLog.w(TAG, "apk entry is null. path:" + patchRealPath);
                        manager.getPatchReporter().onPatchTypeExtractFail(patchFile, extractedFile, info.rawName, type);
                        return false;
                    }
                    // old dex crc校验(补丁包中记录的old dex crc和当前apk中old dex crc)
                    String rawEntryCrc = String.valueOf(rawApkFileEntry.getCrc());
                    if (!rawEntryCrc.equals(oldDexCrc)) {
                        ShareTinkerLog.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;
                    }
                    // 合成补丁,合成完后写入合成结果dex到extractedFile(data/data/包名/tinker/patch-xxx/dex)
                    // 内部通过DexPatchApplier类合成补丁,算法实现相关代码不具体分析
                    patchDexFile(apk, patch, rawApkFileEntry, patchFileEntry, info, extractedFile);
                    // 校验本次合成成功后dex的md5,是否和打补丁包时预合成的md5一致
                    if (!SharePatchFileUtil.verifyDexFileMd5(extractedFile, extractedFileMd5)) {
                        ShareTinkerLog.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;
                    }

                    ShareTinkerLog.w(TAG, "success recover dex file: %s, size: %d, use time: %d",
                        extractedFile.getPath(), extractedFile.length(), (System.currentTimeMillis() - start));
                }
            }
            // art将所有合成完毕的dex打包为tinker_classN.apk
            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;
    }
}

触发dex2oat编译合成后dex

合成dex成功后需要对每个dex进行dex2oat操作,以免运行时触发dex2oat影响加载速度,tinker借助PathClassLoader触发dex2oat,简略流程如下

  1. DexDiffPatchInternal.dexOptimizeDexFiles方法做一下预操作,然后调用到TinkerDexOptimizer.optimizeAll方法,对每个dex/apk/jar文件分别调用OptimizeWorker.run方法进行dex2oat
  2. android8.0以及以上版本调用NewClassLoaderInjector.triggerDex2Oat方法分别通过DelegateLastClassLoader或者TinkerClassLoader触发dex2oat,8.0以下直接调用DexFile.loadDex
  3. 要注意的是android 10以后不再从应用进程调用dex2oat,仅接受系统生成的OAT文件,所以需要通过triggerPMDexOptOnDemand方法反射调用pms performDexOptSecondary方法再次尝试触发dex2oat

DexDiffPatchInternal.dexOptimizeDexFiles预操作

根据android版本拼接相应dex2oat输出路径,android8.0后是data/data/包名/tinker/patch-xxx/oat//xxx.odex,8.0前是data/data/包名/tinker/patch-xxx/odex/xxx.dex,路径中isa表示cpu架构,比如arm64

private static boolean dexOptimizeDexFiles(Context context, List<File> dexFiles, String optimizeDexDirectory, final File patchFile, final PatchResult patchResult) {
    final Tinker manager = Tinker.with(context);
    optFiles.clear();
    if (dexFiles != null) {
        // data/data/包名/tinker/patch-xxx/odex
        File optimizeDexDirectoryFile = new File(optimizeDexDirectory);
        if (!optimizeDexDirectoryFile.exists() && !optimizeDexDirectoryFile.mkdirs()) {
            return false;
        }
        // add opt files
        for (File file : dexFiles) {
            // 获取dexopt产物输出路径
            // android8.0后是data/data/包名/tinker/patch-xxx/oat/<isa>/xxx.odex
            // 8.0前是data/data/包名/tinker/patch-xxx/odex/xxx.dex
            String outputPathName = SharePatchFileUtil.optimizedPathFor(file, optimizeDexDirectoryFile);
            optFiles.add(new File(outputPathName));
        }
        ......
        // 是否使用DelegateLastClassLoader类
        final boolean useDLC = TinkerApplication.getInstance().isUseDelegateLastClassLoader();
        final boolean[] anyOatNotGenerated = {false};
        // 开始并行dexopt
        TinkerDexOptimizer.optimizeAll(
              context, dexFiles, optimizeDexDirectoryFile,
              useDLC, ......);
        ......
    }
    return true;
}

OptimizeWorker.run分版本触发dex2oat

此方法中NewClassLoaderInjector.triggerDex2Oat函数以及triggerPMDexOptOnDemand会单独拿出来分析。
另外需要注意useInterpretMode时调用interpretDex2Oat以解释模式执行dex2oat,这是为了应对系统OTA升级后补丁odex失效的情况,具体会在加载补丁部分细说

private static class OptimizeWorker {
    boolean run() {
        try {
            ......
            // android8.0后是data/data/包名/tinker/patch-xxx/oat/<isa>/xxx.odex
            // 8.0前是data/data/包名/tinker/patch-xxx/odex/xxx.dex
            String optimizedPath = SharePatchFileUtil.optimizedPathFor(this.dexFile, this.optimizedDir);
            if (!ShareTinkerInternals.isArkHotRuning()) {
                if (useInterpretMode) {
                    // 以解释模式编译,系统OTA后第一次运行时执行此分支
                    interpretDex2Oat(dexFile.getAbsolutePath(), optimizedPath);
                } else if (Build.VERSION.SDK_INT >= 26
                        || (Build.VERSION.SDK_INT >= 25 && Build.VERSION.PREVIEW_SDK_INT != 0)) {
                    // 通过PathClassLoader/加载dex触发dex2oat
                    NewClassLoaderInjector.triggerDex2Oat(context, optimizedDir,
                                                          useDLC, dexFile.getAbsolutePath());
                    // https://developer.android.google.cn/about/versions/10/behavior-changes-10?hl=zh-cn#system-only-oat
                    // android10以后不再从应用进程调用dex2oat,仅接受系统生成的OAT文件
                    // oat_file_manager.cc  OatFileManager::OpenDexFilesFromOat不再调用oat_file_assistant.MakeUpToDate
                    // 这里通过pms再次触发后台dex2oat
                    triggerPMDexOptOnDemand(context, dexFile.getAbsolutePath(), optimizedPath);
                } else {
                    // android8.0 之下直接使用DexFile触发dex2oat
                    DexFile.loadDex(dexFile.getAbsolutePath(), optimizedPath, 0);
                }
            }
            if (callback != null) {
                callback.onSuccess(dexFile, optimizedDir, new File(optimizedPath));
            }
        } catch (final Throwable e) {
            if (callback != null) {
                callback.onFailed(dexFile, optimizedDir, e);
                return false;
            }
        }
        return true;
    }
}

通过PathClassLoader触发dex2oat

NewClassLoaderInjector.triggerDex2Oat调用到NewClassLoaderInjector.createNewClassLoader,在useDLC=true并且android版本大于8.1时使用DelegateLastClassLoader触发dex2oat,否则创建TinkerClassLoader触发dex2oat。 实际上触发dex2oat这一步时这两个ClassLoader起到的效果是一样的,最后都是通过DexPathList触发dex2oat,新创建的ClassLoader仅仅用于触发dex2oat。

NewClassLoaderInjector.createNewClassLoader另外一个调用处在加载补丁逻辑中,加载补丁时使用新classerLoader替换Application原有PathClassLoader避免android7.0之后以混合编译与对热补丁影响。所以我觉得此方法还是应该分开写成两个函数比较好,以免阅读代码时产生混淆。
此文章中我们只需要知道不管是DelegateLastClassLoader还是TinkerClassLoader作用都是触发dex2oat就好了,对于它们更进一步的分析留到下篇加载补丁时分析。

DelegateLastClassLoader是android8.1后新增的类,继承于PathClassLoader,实行最后查找策略。

  1. 从boot classpath中查找类
  2. 从该classLoader的dexPath中查找类
  3. 最后从该classLoader的双亲中查找类
  4. 最后查找策略没有遵守双亲委托,最后才从parent classloader中查找类

TinkerClassLoader在这里起的作用也差不多,重写了findClass方法使查找类时先从TinkerClassLoader查找,然后再到原来的PathClassLoader中查找

private static ClassLoader createNewClassLoader(ClassLoader oldClassLoader,
                                                    File dexOptDir,
                                                    boolean useDLC,
                                                    String... patchDexPaths) throws Throwable {
        // 反射获取BaseDexClassLoader的DexPathList字段
        // old oldClassLoader是当前app的PathClassLoader
        final Field pathListField = findField(
                Class.forName("dalvik.system.BaseDexClassLoader", false, oldClassLoader),
                "pathList");
        final Object oldPathList = pathListField.get(oldClassLoader);

        final StringBuilder dexPathBuilder = new StringBuilder();
        final boolean hasPatchDexPaths = patchDexPaths != null && patchDexPaths.length > 0;
        if (hasPatchDexPaths) {
            for (int i = 0; i < patchDexPaths.length; ++i) {
                if (i > 0) {
                    dexPathBuilder.append(File.pathSeparator);
                }
                dexPathBuilder.append(patchDexPaths[i]);
            }
        }
        // 拼接需要dex2oat的dex文件路径
        final String combinedDexPath = dexPathBuilder.toString();

        // 反射DexPathList中nativeLibraryDirectories字段,so库路径
        final Field nativeLibraryDirectoriesField = findField(oldPathList.getClass(), "nativeLibraryDirectories");
        ......
        // 拼接so库路径
        final String combinedLibraryPath = libraryPathBuilder.toString();

        ClassLoader result = null;
        if (useDLC && Build.VERSION.SDK_INT >= 27) {
            // https://developer.android.google.cn/reference/dalvik/system/DelegateLastClassLoader
            // https://www.androidos.net.cn/android/10.0.0_r6/xref/libcore/dalvik/src/main/java/dalvik/system/DelegateLastClassLoader.java
            // DelegateLastClassLoader是android8.1后新增的,继承于PathClassLoader,实行最后查找策略
            result = new DelegateLastClassLoader(combinedDexPath, combinedLibraryPath, ClassLoader.getSystemClassLoader());
            // 将之前的PathClassLoader设为创建的DelegateLastClassLoader的双亲
            final Field parentField = ClassLoader.class.getDeclaredField("parent");
            parentField.setAccessible(true);
            parentField.set(result, oldClassLoader);
        } else {
            // 做的事情和DelegateLastClassLoader差不多
            result = new TinkerClassLoader(combinedDexPath, dexOptDir, combinedLibraryPath, oldClassLoader);
        }

        // Android8.0之前版本替换原本的PathClassLoader中PathList中的classLoader为新创建的
        // Android 8.0之后不支持多个类加载器同时使用同一个DexFile对象来定义类,所以不能替换
        if (Build.VERSION.SDK_INT < 26) {
            findField(oldPathList.getClass(), "definingContext").set(oldPathList, result);
        }

        return result;
    }

triggerPMDexOptOnDemand确保在Android10以后能够触发dex2oat

由于android10对于dex2oat策略的变更,这里必须要做进一步处理,tinker主要大概流程如下

  1. queryPerformDexOptSecondaryTransactionCode方法反射获取用于标识PackageManagerService.performDexOptSecondary方法的transaction code
  2. 反射ServiceManager拿到PMS客户端代理IBinder对象
  3. 通过binder调用PackageManagerService.performDexOptSecondary,以quicken模式触发dex2oat
private static void triggerPMDexOptOnDemand(Context context, String dexPath, String oatPath) {
    if (Build.VERSION.SDK_INT < 29) {
        return;
    }
    try {
        final File oatFile = new File(oatPath);
        if (oatFile.exists()) {
            return;
        }
        boolean performDexOptSecondarySuccess = true;
        try {
            // 调用PMS.performDexOptSecondary触发dex2oat
            performDexOptSecondary(context, oatPath);
        } catch (Throwable thr) {
            performDexOptSecondarySuccess = false;
        }
        SystemClock.sleep(1000);
        if (!performDexOptSecondarySuccess || !oatFile.exists()) {
            // 执行失败,如果是华为系统做额外处理
            if ("huawei".equalsIgnoreCase(Build.MANUFACTURER) || "honor".equalsIgnoreCase(Build.MANUFACTURER)) {
                registerDexModule(context, dexPath, oatPath);
            }
        }
        ......
}
public static void performDexOptSecondary(Context context, String oatPath) throws IllegalStateException {
    try {
        final File oatFile = new File(oatPath);
        // 反射获取pms代理用于ipc标记pms.performDexOptSecondary方法的transactionCode
        final int transactionCode = queryPerformDexOptSecondaryTransactionCode();
        final String packageName = context.getPackageName();
        // dex2oat编译模式
        final String targetCompilerFilter = "quicken";
        final boolean force = false;

        final Class<?> serviceManagerClazz = Class.forName("android.os.ServiceManager");
        final Method getServiceMethod = ShareReflectUtil.findMethod(serviceManagerClazz, "getService", String.class);
        // 拿到PMS远程代理
        final IBinder pmBinder = (IBinder) getServiceMethod.invoke(null, "package");
        if (pmBinder == null) {
            throw new IllegalStateException("Fail to get pm binder.");
        }
        // 重试20次
        final int maxRetries = 20;
        for (int i = 0; i < maxRetries; ++i) {
            Throwable pendingThr = null;
            try {
                // 直接通过binder调用pms performDexOptSecondary
                performDexOptSecondaryImpl(pmBinder, transactionCode, packageName, targetCompilerFilter, force);
            } catch (Throwable thr) {
                pendingThr = thr;
            }
            SystemClock.sleep(3000);
            if (oatFile.exists()) {
                break;
            }
            if (i == maxRetries - 1) {
                if (pendingThr != null) {
                    throw pendingThr;
                }
                if (!oatFile.exists()) {
                    throw new IllegalStateException("Expected oat file: " + oatFile.getAbsolutePath() + " does not exist.");
                }
            }
        }
        ......
}

对于PackageManagerService.performDexOptSecondary方法的实现这里就不再展开了,有兴趣的话可以自行去阅读源码,自此补丁合成过程已分析完毕。

后记

跟着源码一路看下来很明显可以感觉到作者对于framework层的了解,不同android版本中framework的变动对于tinker的影响非常大,厂商对于ROM的定制化修改也会引出一系列问题,解决这些问题实在不算容易的事情。对于一些细节问题我也没有完全了解,如果有错误希望指出。