前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Tinker源码分析(六):补丁合成流程

Tinker源码分析(六):补丁合成流程

作者头像
俞其荣
发布2019-03-19 17:14:11
1.3K0
发布2019-03-19 17:14:11
举报
文章被收录于专栏:俞其荣的博客俞其荣的博客

本系列 Tinker 源码解析基于 Tinker v1.9.12

补丁合成流程

下发的补丁包其实并不能直接加载,因为补丁包只是差异包,需要和本地的 dex 、资源等进行合成后,得到全量的 dex 才能被完整地使用。这样也就避免了热修复中 dex 的 pre-verify 问题,也减少了补丁包的体积,方便用户下载。

补丁合成的入口在 TinkerInstaller.onReceiveUpgradePatch 方法

TinkerInstaller.onReceiveUpgradePatch

代码语言:javascript
复制
public static void onReceiveUpgradePatch(Context context, String patchLocation) {
    Tinker.with(context).getPatchListener().onPatchReceived(patchLocation);
}

这里的 PatchListener 有默认实现类,即 DefaultPatchListener 。

DefaultPatchListener.onPatchReceived

代码语言:javascript
复制
@Override
public int onPatchReceived(String path) {
    File patchFile = new File(path);
    // 对补丁进行校验
    int returnCode = patchCheck(path, SharePatchFileUtil.getMD5(patchFile));

    if (returnCode == ShareConstants.ERROR_PATCH_OK) {
        // 通过的话就启动 :process 进程进行补丁合成    
        TinkerPatchService.runPatchService(context, path);
    } else {
        // 校验失败就回调 onLoadPatchListenerReceiveFail
        Tinker.with(context).getLoadReporter().onLoadPatchListenerReceiveFail(new File(path), returnCode);
    }
    return returnCode;
}

我们直接来看 TinkerPatchService.runPatchService 方法

TinkerPatchService.runPatchService

代码语言:javascript
复制
public static void runPatchService(final Context context, final String path) {
    TinkerLog.i(TAG, "run patch service...");
    Intent intent = new Intent(context, TinkerPatchService.class);
    // path 就是补丁的路径
    intent.putExtra(PATCH_PATH_EXTRA, path);
    // RESULT_CLASS_EXTRA 一般默认就是 DefaultTinkerResultService
    intent.putExtra(RESULT_CLASS_EXTRA, resultServiceClass.getName());
    try {
        enqueueWork(context, TinkerPatchService.class, JOB_ID, intent);
    } catch (Throwable thr) {
        TinkerLog.e(TAG, "run patch service fail, exception:" + thr);
    }
}

在 runPatchService 中去启动了 TinkerPatchService 。TinkerPatchService 是跑在 :patch 进程中的。

TinkerPatchService 主要做的事情都在 onHandleWork 中

代码语言:javascript
复制
@Override
protected void onHandleWork(Intent intent) {
    // 提高优先级
    increasingPriority();
    // 合成补丁
    doApplyPatch(this, intent);
}

首先是 increasingPriority 方法,目的就是提高 service 的优先级,具体的方案就是设置为前台服务

代码语言:javascript
复制
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);
    }
}

接着是 doApplyPatch 方法,在这里做补丁合成的事

代码语言:javascript
复制
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)) {
        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;
    // 补丁合成的结果回调给 DefaultResultService
    AbstractResultService.runResultService(context, patchResult, getPatchResultExtra(intent));

    sIsPatchApplying.set(false);
}

upgradePatchProcessor 是一个接口,具体的实现类是 UpgradePatch 。

UpgradePatch.tryPatch

那么来看看 UpgradePatch.tryPatch ,方法比较长,分段来看吧。

首先是对 Tinker 自身开关的校验,然后对补丁文件的合法性进行校验。

代码语言:javascript
复制
@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;
    }

然后检查补丁包的签名以及 tinkerId , 这里的操作和加载补丁是一样的。

然后就是获取补丁文件的 md5 值

代码语言:javascript
复制
//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;
}

// 获取补丁文件的 md5
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
// 用 md5 做版本号
patchResult.patchVersion = patchMd5;

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

接着,校验完成后,我们就来构造一个新的 patch.info 文件了。

代码语言:javascript
复制
//check ok, we can real recover a new patch
final String patchDirectory = manager.getPatchDirectory().getAbsolutePath();

// info.lock 文件
File patchInfoLockFile = SharePatchFileUtil.getPatchInfoLockFile(patchDirectory);
// patch.info 文件
File patchInfoFile = SharePatchFileUtil.getPatchInfoFile(patchDirectory);

// 读取出老的 patch.info 文件,可能存在 可能不存在
SharePatchInfo oldInfo = SharePatchInfo.readAndCheckPropertyWithLock(patchInfoFile, patchInfoLockFile);

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

//如果有老的 patch.info 文件
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;
    // 构造新的 patch.info 
    newInfo = new SharePatchInfo(oldInfo.oldVersion, patchMd5, false, Build.FINGERPRINT, finalOatDir);
} else {
    // 构造新的 patch.info 
    newInfo = new SharePatchInfo("", patchMd5, false, Build.FINGERPRINT, ShareConstants.DEFAULT_DEX_OPTIMIZE_PATH);
}

再接下来,就是把补丁包复制到私有目录中

具体的路径也就是之前在加载补丁中遇到的 /data/data/应用包名/tinker/patch-xxxxxx/patch-xxxxxx.apk

代码语言:javascript
复制
//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))) {
      // 复制补丁包到 /data/data/ 中
      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;
}

复制好之后,就是把补丁包和基准包进行整合了

代码语言:javascript
复制
//we use destPatchFile instead of patchFile, because patchFile may be deleted during the patch process
// 合成 dex
if (!DexDiffPatchInternal.tryRecoverDexFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {
    TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch dex failed");
    return false;
}
// 合成 so 文件
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;
}

这里面的三个合成代码我们到后面的章节再分析,这里先跳过了。

合成完后,还要对 dex 进行opt优化

代码语言:javascript
复制
// 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;
}

最后,就是把结果重新写入到 patch.info ,这样在加载补丁的流程中就能加载新补丁了。

代码语言:javascript
复制
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;

over ,整个合成补丁的流程讲完了,这里还留了三个坑:

  • dex 文件的合成
  • so 文件的合成
  • 资源文件的合成

到后面再讲吧。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019.03.15 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 补丁合成流程
  • TinkerInstaller.onReceiveUpgradePatch
  • DefaultPatchListener.onPatchReceived
  • TinkerPatchService.runPatchService
  • UpgradePatch.tryPatch
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档