tinker热修复——补丁加载合成

《Android tinker热修复——实战接入项目》看了该文章的读者应该懂得如何使用tinker接入自己的项目了,但是作为开发人员,会使用人家的框架还远远不够,我们需要学习别人的设计思想和原理,来提高自己。

在会使用了tinker的基础上,接下来我们深入学习一下tinker到底是如何工作,如何加载合成补丁的,如何修复bug的。由于tinker热修复框架比较强大,而且原理和思想比较深,所以一篇文章去探索是远远不够的,我们需要一个步骤一个步骤的解析,而该片文章主要是探析tinker的补丁加载合成。

我们看下tinker的工作流程:

接下来我们分析一下源码的实现过程,这里我们挑了合成patch的流程。

开始补丁合成

下载完成补丁之后,将要调用TinkerInstaller的onReceiveUpgradePatch方法。

安装合成新的补丁包,并且启动补丁进程。

Tinker.with(context):

这里主要是初始化tinker的基本配置,并且使用同步机制来初始化配置。

new Builder(context).build():

初始化的配置,我都已经在代码中注释好了。在build()方法的最后,new了Tinker对象,而在Tinker构造方法里面主要是将刚刚初始化的默认配置,缓存到Tinker里面。

我们看回onReceiveUpgradePatch方法里面调用的getPatchListener,而该方法主要是返回刚刚在build里面初始化的DefaultPatchListener对象。

DefaultPatchListener就是检查修复包的类,最后调用该类的onPatchReceived,该方法开始检查修复包,检查完成之后就开始合成补丁包。

在onPatchReceived里面调用patchCheck方法检查修复包

 /**
     * when we receive a patch, what would we do?
     * you can overwrite it
     *
     * @param path 补丁路径
     * @return
     */
    @Override
    public int onPatchReceived(String path) {
        File patchFile = new File(path);

        int returnCode = patchCheck(path, SharePatchFileUtil.getMD5(patchFile));

        //一切准备好之后,调用runPatchService方法,启动补丁的Service服务
        if (returnCode == ShareConstants.ERROR_PATCH_OK) {
            TinkerPatchService.runPatchService(context, path);
        } else {
            Tinker.with(context).getLoadReporter().onLoadPatchListenerReceiveFail(new File(path), returnCode);
        }
        return returnCode;
    }

    /**
     *
     * @param path 补丁路径
     * @param patchMd5 //补丁文件的md5值
     * @return
     */
    protected int patchCheck(String path, String patchMd5) {
        Tinker manager = Tinker.with(context);
        //check SharePreferences also
        //检查是否开启补丁修复
        if (!manager.isTinkerEnabled() || !ShareTinkerInternals.isTinkerEnableWithSharedPreferences(context)) {
            return ShareConstants.ERROR_PATCH_DISABLE;
        }
        File file = new File(path);

        //检查补丁是否存在
        if (!SharePatchFileUtil.isLegalFile(file)) {
            return ShareConstants.ERROR_PATCH_NOTEXIST;
        }

        //patch service can not send request
        //补丁服务不能发送请求
        if (manager.isPatchProcess()) {
            return ShareConstants.ERROR_PATCH_INSERVICE;
        }

        //if the patch service is running, pending
        //如果补丁服务的servicec正在运行,就等待
        if (TinkerServiceInternals.isTinkerPatchServiceRunning(context)) {
            return ShareConstants.ERROR_PATCH_RUNNING;
        }
        //检查JIT
        if (ShareTinkerInternals.isVmJit()) {
            return ShareConstants.ERROR_PATCH_JIT;
        }

        //获取tinker对象
        Tinker tinker = Tinker.with(context);

        //tinker加载完成
        if (tinker.isTinkerLoaded()) {
            //加载完成结果
            TinkerLoadResult tinkerLoadResult = tinker.getTinkerLoadResultIfPresent();
            if (tinkerLoadResult != null && !tinkerLoadResult.useInterpretMode) {
                String currentVersion = tinkerLoadResult.currentVersion;
                //补丁没有变和当前版本相同
                if (patchMd5.equals(currentVersion)) {
                    return ShareConstants.ERROR_PATCH_ALREADY_APPLY;
                }
            }
        }

        if (!UpgradePatchRetry.getInstance(context).onPatchListenerCheck(patchMd5)) {
            return ShareConstants.ERROR_PATCH_RETRY_COUNT_LIMIT;
        }

        return ShareConstants.ERROR_PATCH_OK;
    }
复制代码

经过一系列的检查,包括检查是否开启补丁修复、补丁是否存在和补丁服务是否正在运行等等,最后返回ERROR_PATCH_OK表示补丁验证通过。

补丁的检验通过之后会调用TinkerPatchService.runPatchService来启动合成补丁的服务。否则调用DefaultLoadReporter的onLoadPatchListenerReceiveFail方法,报告加载补丁包失败。

//一切准备好之后,调用runPatchService方法,启动补丁的Service服务
        if (returnCode == ShareConstants.ERROR_PATCH_OK) {
            TinkerPatchService.runPatchService(context, path);
        } else {
            Tinker.with(context).getLoadReporter()
                    .onLoadPatchListenerReceiveFail
                            (new File(path), returnCode);
        }
复制代码

我们主要看TinkerPatchService的runPatchService方法,启动服务。

该方法接受的参数就是将补丁的路径和上下文传过去,启动TinkerPatchService服务,而TinkerPatchService该服务是属于:patch进程的。TinkerPatchService继承IntentService,需要重写onHandleIntent方法,该方法是运行在子线程当中,可以做一些耗时任务,任务完成之后会自己结束掉服务。

启动服务之后,我们主要看TinkerPatchService的onHandleIntent方法。

@Override
    protected void onHandleIntent(Intent intent) {
        final Context context = getApplicationContext();
        //获取tinker对象
        Tinker tinker = Tinker.with(context);
        //调用DefaultPatchReporter的onPatchServiceStart方法
        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);
        //获取从设备boot后经历的时间值。
        long begin = SystemClock.elapsedRealtime();
        boolean result;
        long cost;
        Throwable e = null;

        increasingPriority();
        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;

        //关闭patch进程,如果更新成功就要删掉rawPatchFilePath
        AbstractResultService.runResultService(
                context,
                patchResult, 
                getPatchResultExtra(intent));

    }
复制代码

在onHandleIntent方法里面,首先会调用DefaultPatchReporter的onPatchServiceStart方法,而DefaultPatchReporter主要是打修复包过程中的报告类。看下onPatchServiceStart方法做了那些事情。

主要做的工作就是报告TinkerPatchService开始时的一些工作。

继续往下看,最后调用UpgradePatchRetry的onPatchServiceStart方法。

/**
     * 启动服务要做的事情
     * 包括把文件搬到/data/data/包名下
     * @param intent
     */
    public void onPatchServiceStart(Intent intent) {
        if (!isRetryEnable) {
            TinkerLog.w(TAG, 
                    "onPatchServiceStart retry disabled, just return");
            return;
        }

        if (intent == null) {
            TinkerLog.e(TAG, 
                    "onPatchServiceStart intent is null, just return");
            return;
        }

        String path = TinkerPatchService.getPatchPathExtra(intent);

        if (path == null) {
            TinkerLog.w(TAG, 
                    "onPatchServiceStart patch path is null, just return");
            return;
        }

        RetryInfo retryInfo;
        File patchFile = new File(path);

        String patchMd5 = SharePatchFileUtil.getMD5(patchFile);
        if (patchMd5 == null) {
            TinkerLog.w(TAG, 
                    "onPatchServiceStart patch md5 is null, just return");
            return;
        }

        if (retryInfoFile.exists()) {
            retryInfo = RetryInfo.readRetryProperty(retryInfoFile);
            if (retryInfo.md5 == null || 
                    retryInfo.times == null || 
                    !patchMd5.equals(retryInfo.md5)) {
                copyToTempFile(patchFile);
                retryInfo.md5 = patchMd5;
                retryInfo.times = "1";
            } else {
                int nowTimes = Integer.parseInt(retryInfo.times);
                if (nowTimes >= maxRetryCount) {
                    SharePatchFileUtil.safeDeleteFile(tempPatchFile);
                    TinkerLog.w(TAG, 
                            "onPatchServiceStart retry more than max count" +
                            ", delete retry info file!");
                    return;
                } else {
                    retryInfo.times = String.valueOf(nowTimes + 1);
                }
            }

        } else {
            copyToTempFile(patchFile);
            retryInfo = new RetryInfo(patchMd5, "1");
        }

        //重写属性
        RetryInfo.writeRetryProperty(retryInfoFile, retryInfo);
    }
复制代码

该方法也经过一些列的判断,往下看,我们看到copyToTempFile(patchFile)这个方法。

方法内调用SharePatchFileUtil.copyFileUsingStream方法。

/**
     * 将补丁文件copy到dest文件下
     * @param source
     * @param dest
     * @throws IOException
     */
    public static void copyFileUsingStream(File source, 
                                           File dest)
            throws IOException {
        if (!SharePatchFileUtil.isLegalFile(source) || 
                dest == null) {
            return;
        }
        if (source.getAbsolutePath().
                equals(dest.getAbsolutePath())) {
            return;
        }
        FileInputStream is = null;
        FileOutputStream os = null;
        File parent = dest.getParentFile();
        if (parent != null && (!parent.exists())) {
            parent.mkdirs();
        }
        try {
            is = new FileInputStream(source);
            os = new FileOutputStream(dest, false);

            byte[] buffer = new byte[ShareConstants.
                    BUFFER_SIZE];
            int length;
            while ((length = is.read(buffer)) > 0) {
                os.write(buffer, 0, length);
            }
        } finally {
            closeQuietly(is);
            closeQuietly(os);
        }
    }
复制代码
该方法就是将补丁文件copy到dest文件,在UpgradePatchRetry创建对象的时候就初始化了tempPatchFile文件(/data/data/包名/tinker_temp/temp.apk)。

看回onPatchServiceStart方法,最后调用writeRetryProperty方法。

更新RetryInfo对象的属性,存储着补丁的md5和time。

onPatchServiceStart:该方法主要的作用就是将补丁文件拷贝一份到(/data/data/包名/tinker_temp/temp.apk)temp.apk文件,然后更新补丁文件的md5和time并且存储起来。

继续看onHandleIntent方法,经过intent判断,通过onHandleIntent方法拿到path之后,然后通过increasingPriority()方法,将服务设置到前台来,为了就是让该无法不被系统杀死。

service设置到前台

startForeground(notificationId, notification)

startForeground(notificationId, notification):后台服务置于前台,就像音乐播放器的播放服务一样,不会被系统杀死。如果Build.VERSION.SDK_INT > 18,开启一个InnerService降低被杀死的概率。
将service调到前台之后,接着调用UpgradePatch的tryPatch方法,该方法就是合成补丁包的过程。
@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)) {
            TinkerLog.e(TAG, 
                    "UpgradePatch tryPatch:patch is disabled, just return");
            return false;
        }

        if (!SharePatchFileUtil.isLegalFile(patchFile)) {
            TinkerLog.e(TAG, 
                    "UpgradePatch tryPatch:patch file is not found, just return");
            return false;
        }
        //check the signature, we should create a new checker
        ShareSecurityCheck signatureCheck = new ShareSecurityCheck(context);

        int returnCode = ShareTinkerInternals.checkTinkerPackage(context, 
                manager.getTinkerFlags(), patchFile, signatureCheck);
        if (returnCode != ShareConstants.ERROR_PACKAGE_CHECK_OK) {
            TinkerLog.e(TAG, "UpgradePatch tryPatch:onPatchPackageCheckFail");
            manager.getPatchReporter().onPatchPackageCheckFail(patchFile, 
                    returnCode);
            return false;
        }

        String patchMd5 = SharePatchFileUtil.getMD5(patchFile);
        if (patchMd5 == null) {
            TinkerLog.e(TAG, 
                    "UpgradePatch tryPatch:patch md5 is null, just return");
            return false;
        }
        //use md5 as version
        patchResult.patchVersion = patchMd5;

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

        //check ok, we can real recover a new patch
        final String patchDirectory = manager
                .getPatchDirectory()
                .getAbsolutePath();

        File patchInfoLockFile = SharePatchFileUtil
                .getPatchInfoLockFile(patchDirectory);
        File patchInfoFile = SharePatchFileUtil
                .getPatchInfoFile(patchDirectory);

        SharePatchInfo oldInfo = SharePatchInfo
                .readAndCheckPropertyWithLock(
                        patchInfoFile, 
                        patchInfoLockFile);

        //it is a new patch, so we should not find a exist
        SharePatchInfo newInfo;

        //already have patch
        if (oldInfo != null) {
            if (oldInfo.oldVersion == null || 
                    oldInfo.newVersion == null || 
                    oldInfo.oatDir == null) {
                TinkerLog.e(TAG, 
                        "UpgradePatch tryPatch:onPatchInfoCorrupted");
                manager.getPatchReporter()
                        .onPatchInfoCorrupted(
                                patchFile, 
                                oldInfo.oldVersion, 
                                oldInfo.newVersion);
                return false;
            }

            if (!SharePatchFileUtil.checkIfMd5Valid(patchMd5)) {
                TinkerLog.e(TAG, 
                        "UpgradePatch tryPatch:onPatchVersionCheckFail " +
                                "md5 %s is valid", patchMd5);
                manager.getPatchReporter().
                        onPatchVersionCheckFail(
                                patchFile, 
                                oldInfo, 
                                patchMd5);
                return false;
            }
            // if it is interpret now, use changing flag to wait main process
            final String finalOatDir = oldInfo
                    .oatDir
                    .equals(ShareConstants
                            .INTERPRET_DEX_OPTIMIZE_PATH)
                ? ShareConstants.CHANING_DEX_OPTIMIZE_PATH : oldInfo.oatDir;
            newInfo = new SharePatchInfo(
                    oldInfo.oldVersion, 
                    patchMd5, 
                    Build.FINGERPRINT, 
                    finalOatDir);
        } else {
            newInfo = new SharePatchInfo(
                    "", 
                    patchMd5, 
                    Build.FINGERPRINT, 
                    ShareConstants.DEFAULT_DEX_OPTIMIZE_PATH);
        }

        //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))) {
                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) {
//            e.printStackTrace();
            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
        if (!DexDiffPatchInternal.tryRecoverDexFiles(
                manager, 
                signatureCheck, 
                context, 
                patchVersionDirectory, 
                destPatchFile)) {
            TinkerLog.e(TAG, 
                    "UpgradePatch tryPatch:new patch recover," +
                            " try patch dex failed");
            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;
        }

        TinkerLog.w(TAG, "UpgradePatch tryPatch: done, it is ok");
        return true;
    }
复制代码

微信团队写代码是非常严谨的,在tryPatch方法中,需要经过一系列的判断(包括判断是否开启热修复,是否存在补丁包,md5是否为null等)之后,然后调用补丁合成的方法。

在此之前new了一个名为destPatchFile的文件,如果之前没有存在该文件,就需要将补丁复制写入,合成的时候需要用destPatchFile文件,因为如果使用原来的补丁文件,在合成的过程中,用户有可能删除补丁,所以为了安全需要使用destPatchFile文件来进行合成。

注意:

1、补丁包必须拷贝到/data/data/包名/目录下,通过从下载目录文件通过流读出写入到该目录下,因为修复替换patch需要在/data/data/包名/目录下进行。

2、在合成的时候,分别使用DexDiff合成dex、BsDiff合成library和ResDiff合成resource。

最后一部就是拷贝SharePatchInfo到PatchInfoFile中,使用SharePatchInfo的rewritePatchInfoFileWithLock方法。

执行完补丁的合成之后,在TinkerPatchService的onHandleIntent方法中,会调用AbstractResultService的runResultService方法,而runResultService方法启动的service就是我们在调用tinker.install方法传入的service。在TinkerSample中传入的是SampleResultService,而SampleResultService的onPatchResult方法,主要做的就是:

1、调用killTinkerPatchServiceProcess关闭patchService的进程

2、调用deleteRawPatchFile删掉补丁文件。

以上就是补丁合成的过程,但是没有深入到合成算法的分析,简单的合成流程分析,算法还需要时间慢慢啃。

参考文章:《微信热补丁Tinker – 补丁流程》

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值