美文网首页Android开发实战总结其他Android知识
# Tinker学习计划(2)-Tinker的原理一

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

作者: 徐正峰 | 来源:发表于2017-07-25 09:38 被阅读357次

    前言

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

    • Tinker的结构(主要是分析Tinker的架构,从宏观层面来了解Tinker)
    • Tinker代码修复的原理(描述补丁包生效的过程,源码分析)
    • Tinker资源修复的原理(Tinker资源构建的原理,及修复的原理,源码层面)
    • Tinker对.SO是如何处理的(源码层面)
    • tinker-patch-gradle-plugin源码解析
    • DexDiff算法(源码层面)

    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下文件结构图。

    • info.lock这个文件是为了解决跨进程读写patch.info这个文件所建立的文件锁
    • patch.info说白了是一个配置文件,里面是处理完补丁包以后的一些配置信息,当进程重启以后,patch以外的其他进程会去读这个文件数据来获取当前是否要加载patch的合成包,cat一下这个文件


      说白了就是一个�key-value形式的ini配置文件,上面#号开头的是注释,可以不用管,dir对应的是dex目录,print是当前rom信息,为了判断OTA升级用的,new和old是版本,tinker的版本是以MD5来做处理的。

    • patch-xxxx 目录下面存储的是补丁包的具体数据,patch后面那串数字实际上是补丁包MD5的0到8位。至于为什么是0-8位,是协议层面的事情,暂不撰述。
    • patch-xxxx.apk就是补丁包,从云端下载复制到改目录下的。
    • dex目录,目录中是从patch.apk中抽取的dex文件,至于为什么以jar结尾(实际上也是dex_meta.txt这个文件中来控制的),原因暂且不明,而为什么在编译的的时候做这个事,猜测可能是由于兼容性(davalik和art).
    • odex目录,是通过dex目录中dex文件经过优化Dex2oat而来。两种方式,一种是DexFile.loadDex(**),第二种是通过在代码中构建命令行来进行优化。

    那如果是多个补丁包呢?

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

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

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

    • 安全校验,这个很好理解
    • 作为当前patch的版本号,以及文件夹以MD5作为标识,类似于Patch-xxxx
    • 多个补丁包更新时,判断两个补丁包是否一致

    获取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做了哪些事情。下一节我们继续分析。

    相关文章

      网友评论

        本文标题:# Tinker学习计划(2)-Tinker的原理一

        本文链接:https://www.haomeiwen.com/subject/hrfwkxtx.html