Android开发实战总结其他Android知识

# Tinker学习计划(2)-Tinker的原理一

2017-07-25  本文已影响357人  徐正峰

前言

Tinker学习计划(1)-Tinker的集成 这边文章中我们首先学习了如何去集成Tinker热更新框架,去实现我们自己App的热更新功能。这篇文章主要是从架构和源码的角度去理解Tinker。我计划是分成以下几步:

Tinker的结构

包结构

Tinker的集成 这篇文章,我们的思路基本是按照Tinker Demo里提供的思路来集成,说白了就是通过在Gradle里Compile腾讯上传的代码,这的确能达成目的,但是对于学习却是不方便的,虽然能看到源码,但是.class文件在IDE里操作起来还是很麻烦的,所以在分析结构和学习源码之前,我们重新打个工程,这个工程里不会像下面这样来应用Lib了:

compile('com.tencent.tinker:tinker-android-lib:1.7.11')
provided("com.tencent.tinker:tinker-android-anno:1.7.11")

而是直接把Tinker开源的其他代码来搭建一个包含所有源码的工程,讲起来特别高大上实际实现起来很简单,这里就不细讲了。贴几张图查来大家就了解了。

首先你注释掉 app/build.gradle 中上面的两个依赖库。然后拷贝Github上Tinker的库过来。

下面是整个工程的结构:

settings.gradle,修改成如下即可:

同步时,可能会遇到如下问题:

Error:Unable to find method 'org.gradle.api.internal.artifacts.configurations.ConfigurationInternal.getModule()Lorg/gradle/api/internal/artifacts/ModuleInternal;'.
Possible causes for this unexpected error include:<ul><li>Gradle's dependency cache may be corrupt (this sometimes occurs after a network connection timeout.)
<a href="syncProject">Re-download dependencies and sync project (requires network)</a></li><li>The state of a Gradle build process (daemon) may be corrupt. Stopping all Gradle daemons may solve this problem.
<a href="stopGradleDaemons">Stop Gradle build processes (requires restart)</a></li><li>Your project may be using a third-party plugin which is not compatible with the other plugins in the project or the version of Gradle requested by the project.</li></ul>In the case of corrupt Gradle processes, you can also try closing the IDE and then killing all Java processes.

解决该问题:只需要修改以下两个地方:

可能有的童鞋还是不太了解,索性简单花了一个图来表示各个库的依赖关系:

tinker_android_anno :

工程结构如下:

简单的看了下,实际上这是一个为了在编译期间生成Application的类,至于原理很简单,使用者通过DefaultLifeCycle这个注解来填充一些数据,AnnotationProcessor是集成AbstractProcessor这个类的,这个库的作用也就是说在编译完成以后,自动给工程填充了一个Application类,如下:

关于AbstractProcessor原理介绍,可参考文章:
AbstractProcessor参考

tinker-android-lib

跟Tinker框架相关的类及代码都是在Module中,我们业务开发包括集成也只需要关系这个Module即可。

tinker-commons

从名称就可以看出该Module是业务无关的,里面主要是DexPatch相关的代码,包括Tinker是DexDiff核心的部分,后期我们会详细分析。

third-party:aosp_dexutils

主要包含tinker是如何定义Dex的,这个也是业务无关的

third-party:bsdiff_util

bsdiff相关的代码,在某些情况下,微信的Dex合成实际上用的是Bsdiff算法,当然做了优化。

框架原理

这章我们主要对Tinker的框架进行分析,从宏观层面了解tinker的构造,我尽量讲的详细,这是站在我的角度来思考这个问题,有可能和原作者思想有出入的地方,大家理解。讲解之前先看一张图:

可以看出,集成了tinker的App框架分成了两类进程,一个是原来具体业务中的进程,一个是tinker需要的Patch进程。他们的分工也很明确,我们先说下patch进程,这个很好理解,类似于我们的Demo比较简单,一个按钮作为加载load补丁包的入口,或许线上的项目可能是由于服务端灰度来控制补丁包的下发,然后再去加载,事实上都是一个意思,而这个加载补丁包的过程就是在patch进程中进行的,为什么这么做应该也是因为为了减少业务进程的开销吧。从图中可以看出,patch进程的作用主要是在合并Dex,即通过DexDiff算法来合并原始Dex和补丁Dex,优化Dex及为了避免在进程重启时做这个事,在合并玩Dex后,即完成dexopt的过程。当然做这些事之前都有一些校验的工作。至于patch上报,说白了就是定义了一个生命周期,在做这些事情当中如果发生一些异常行为,可以上报给其他模块或者服务端,tinker这块是支持自定义的。按照规则实现它的接口即可。

具体业务进程的定位很简单,只要是做了tinker框架的初始化,检查是否存在补丁吧,如果有补丁包,通过hook的方式替换原始Dex的加载。校验模块也是必须的,load的上报和patch的上报类似。至于里面还有一些安全模式的校验,我们后面会在谈到。

那这两个进程是怎么进行协同的,实际上他们是通过文件来进行沟通和传递数据的,由于涉及到多进程来访问文件,所以里面也用到了文件锁来避免出现问题。

这个图只是一个抽象,让大家从上帝角度来了解tinker的工作原理,这个时候只需要知道tinker是这种原理工作的。

patch进程工作的流程图:

现在是不是有感觉了,大概知道patch进程的工作原理了。接下来,开始撸源码了。

TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), "/sdcard/patch.apk");

这个就是补丁包加载的入口,后面的路径就是放补丁包的路径,如果是云端下发的,就是下载的地址。

DefaultPatchListener.java

int returnCode = patchCheck(path);
if (returnCode == ShareConstants.ERROR_PATCH_OK) {
    TinkerPatchService.runPatchService(context, path);
} else {
    Tinker.with(context).getLoadReporter().onLoadPatchListenerReceiveFail(new File(path), returnCode);
}
return returnCode;

protected int patchCheck(String path) {
    Tinker manager = Tinker.with(context);
    //如果有设置项则判断是否启动tinker,包括在Application中传入的参数来决定
    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进程中不处理这个操作
    if (manager.isPatchProcess()) {
        return ShareConstants.ERROR_PATCH_INSERVICE;
    }
    //由于对与一个patch操作,做完了就会kill patch进程,如果当前patch进程正存在,则不处理
    if(TinkerServiceInternals.isTinkerPatchServiceRunning(context)) {
        return ShareConstants.ERROR_PATCH_RUNNING;
    }
    //不支持vm支持jit编译以及apilevel在24以下的机器
    if (ShareTinkerInternals.isVmJit()) {
        return ShareConstants.ERROR_PATCH_JIT;
    }
        return ShareConstants.ERROR_PATCH_OK;
    }
}

这个类很简单,就是做一些基本的验证,如果错了,会丢到LoadReporter这个接口的实现类,至于怎么处理,用户可以自定义。tinker的默认实现只是打印了错误日志而已。这里就不贴代码了。我们按照流程接着往下说,验证成功以后,tinker会启动一个IntentService。处于 com.XXX.XXX:patch 进程中。


public static void runPatchService(Context context, String path) {
try {
      Intent intent = new Intent(context, TinkerPatchService.class);
      intent.putExtra(PATCH_PATH_EXTRA, path);
      intent.putExtra(RESULT_CLASS_EXTRA, resultServiceClass.getName());
      context.startService(intent);
      } catch (Throwable throwable) {
          TinkerLog.e(TAG, "start patch service fail, exception:" + throwable);
      }
    }
}

我们在来看看TinkerPatchService中的OnHanderIntent这个方法。

protected void onHandleIntent(Intent intent) {
        final Context context = getApplicationContext();
        Tinker tinker = Tinker.with(context);
        //patch开始时,上报给patchReporter的实现类
        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;
        //提升进程优先级,尽快让patch操作执行,tinker用了两种不同的方案,感
        //兴趣的童鞋可以看下
        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;
        //patch结果出来后,上报给patchReporter的实现类
        tinker.getPatchReporter().
            onPatchResult(patchFile, result, cost);

        patchResult.isSuccess = result;
        patchResult.rawPatchFilePath = path;
        patchResult.costTime = cost;
        patchResult.e = e;

        AbstractResultService.runResultService(context, patchResult, getPatchResultExtra(intent));

    }
}

可以看出来,这个主要是调用upgradePatchProcessor这个类的方法tryPatch。分析这个之前,我们先聊下PatchReporter这个接口。


public interface PatchReporter {
    void onPatchServiceStart(Intent intent);
    void onPatchPackageCheckFail(File patchFile, int errorCode);
    void onPatchVersionCheckFail(File patchFile, SharePatchInfo oldPatchInfo, String patchFileVersion);
    void onPatchTypeExtractFail(File patchFile, File extractTo, String filename, int fileType);
    void onPatchDexOptFail(File patchFile, List<File> dexFiles, Throwable t);
    void onPatchResult(File patchFile, boolean success, long cost);
    void onPatchException(File patchFile, Throwable e);
    void onPatchInfoCorrupted(File patchFile, String oldVersion, String newVersion);
}

实际上细心的同学就会发现,tinker里很多这种设计,主要也是为了方便开发者自定义一些行为,tinker封装了最核心的那部分代码。比方说patch操作,如果在patch操作中出现一些问题,开发者可以定义其行为。

好,我们接着看patch的流程:


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;

        //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;
}

这个函数体有几个重要的步骤,一个是签名检查:ShareSecurityCheck。然后是MD5检查,然后是Patch的核心部分,也就是DexDiffPatchInternal、BsDiffPatchInternal、ResDiffPatchInternal,他们都是集成BasePatchInternal。我们先看ShareSecurityCheck:

ShareSecurityCheck
他主要是做了这些工作,第一是判断patch包的签名和当前安装的apk的签名是否一致,详细代码在ShareSecurityCheck类的verifyPatchMetaSignature函数。第二就是判断patch包的tinker_id是否和基准包的tinker_id是否一致。这个很好理解,各自取出manefest中的tinker_id的值来坐下equals判断即可。昨晚这些以后,tinker还做了一个判断,如下:


public static int checkPackageAndTinkerFlag(ShareSecurityCheck securityCheck, int tinkerFlag) {
        if (isTinkerEnabledAll(tinkerFlag)) {
            return ShareConstants.ERROR_PACKAGE_CHECK_OK;
        }
        HashMap<String, String> metaContentMap = securityCheck.getMetaContentMap();
        //check dex
        boolean dexEnable = isTinkerEnabledForDex(tinkerFlag);
        if (!dexEnable && metaContentMap.containsKey(ShareConstants.DEX_META_FILE)) {
            return ShareConstants.ERROR_PACKAGE_CHECK_TINKERFLAG_NOT_SUPPORT;
        }
        //check native library
        boolean nativeEnable = isTinkerEnabledForNativeLib(tinkerFlag);
        if (!nativeEnable && metaContentMap.containsKey(ShareConstants.SO_META_FILE)) {
            return ShareConstants.ERROR_PACKAGE_CHECK_TINKERFLAG_NOT_SUPPORT;
        }
        //check resource
        boolean resEnable = isTinkerEnabledForResource(tinkerFlag);
        if (!resEnable && metaContentMap.containsKey(ShareConstants.RES_META_FILE)) {
            return ShareConstants.ERROR_PACKAGE_CHECK_TINKERFLAG_NOT_SUPPORT;
        }

        return ShareConstants.ERROR_PACKAGE_CHECK_OK;
    }

tinkerFlag是Application申明的时候我们初始化的,主要的含义就是当前tinker支持哪些热更新,dex,so,res,还是全部,或者一个都不。那这个函数主要的作用的是什么呢,实际上tinker在构建patch包时,如果资源有更新,会在asset下生成一个文件dex_meta.txt。


如果有资源或者so的更新也会对应生成,so_meta.txt和res_meta.txt。这样就很好理解了,比如说当前tinker我们设置成不支持代码更新,但是代码确在patch包修改了,那当前的patch包我们认为是一个无效的patch包,随之放弃更新。

我们接着说MD5的校验、说这个之前,我们先彻底的分析一下,tinker存储patch的文件结构,下面是一个做过一次热更新的时候的app files下文件结构图。

那如果是多个补丁包呢?

实际上就是在patch-xxxx目录并列的层次下面多一个patch-xxxx目录而已,后面的xxxx就是那个补丁包的md5取 0-8位。只不过patch.info里的old和new字段对应的是最新的那个补丁包的md5值。

说完了文件结构,我们接着上面的MD5校验继续撸。

MD5在Tinker里的作用主要以下几个方面

获取MD5是通过下面这个方法来完成的:


public final static String getMD5(final InputStream is) {
        if (is == null) {
            return null;
        }
        try {
            BufferedInputStream bis = new BufferedInputStream(is);
            MessageDigest md = MessageDigest.getInstance("MD5");
            StringBuilder md5Str = new StringBuilder(32);

            byte[] buf = new byte[ShareConstants.MD5_FILE_BUF_LENGTH];
            int readCount;
            while ((readCount = bis.read(buf)) != -1) {
                md.update(buf, 0, readCount);
            }

            byte[] hashValue = md.digest();

            for (int i = 0; i < hashValue.length; i++) {
                md5Str.append(Integer.toString((hashValue[i] & 0xff) + 0x100, 16).substring(1));
            }
            return md5Str.toString();
        } catch (Exception e) {
            return null;
}

那现在配置文件,源文件都准备好了,就要做下面的dex合成,资源合成了。这块我们留到下篇文章接着细说。

总结

好的,今天的文章就到这里,主要分析了Tinker的结构,tinker的启动流程,以及在做dex合成之前,tinker做了哪些事情。下一节我们继续分析。

上一篇下一篇

猜你喜欢

热点阅读