美文网首页Android开发
Tinker热更新源码接入以及分析

Tinker热更新源码接入以及分析

作者: Hi_Felix | 来源:发表于2018-08-12 14:33 被阅读16次

    Tinker热更新源码接入以及分析

    介绍:tinker是一款由腾讯微信团队提供的热更新软件,有着微信庞大的用户基础,每一版tinker都会使用超过200个版本的微信进行兼容性和稳定性的测试,并且与手机厂商有着深度的合作,每一版tinker发布之前,都会先经过手机厂商的测试,并且微信使用的也是tinker的开源版本。相比较于其他的热更新软件,tinker更具有可靠性。

    1.0 以源码的方式接入Tinker

    Tinker GitHub 连接: https://github.com/Tencent/tinker

    建议阅读前先看一遍官网的github,进行一下了解。

    下载源码zip包进行解压,我们要使用到的就是Tinker的以下几个插件包:

    pic-1.png

    tinrd-party:这个是tinker所使用到的一些工具类插件,例如tinker的核心bsdiff、dex文件处理工具、解压zip文件之类的。

    tinker-android:这个就是tinker接入到android工程中之后的主要功能模块,包含了patch以及loader的部分。

    tinker-build:tinker对本地apk进行分析打包,生成patch文件的主要包。

    tinker-commons:公共包,放了一些公共的patch算法的东西。

    1.1 集成

    首先第一步将这四个model导入到当前的工程中,导入之后肯定会出现编译错误,我这里不罗列错误,就把解决方法罗列一下:

    201808121423052222222.png

    导入项目成功之后,肯定会报找不到grandle下面的这四个文件,如上图,从tinker源码目录把这四个文件拷贝过来就行,其中有一个是push的gradle,在这些model中都有引用,下面这个直接删除就好,如下图:

    201808121423273333333.png

    解决完gradle文件的错误之后,是一些model中的compile引用依赖报错,这是因为android gradle版本造成的,tinker默认使用的2.2版本,而在gradle3中,推荐使用的是implementation和api,具体原由不赘述了,
    我是采用的直接改成api,这里tinker官方sample里面有更好的集成方式:

    4.png 5.png

    判断gradle的版本,而后使用不同的集成依赖的方式。

    到这一步,应该源码包就没有什么问题了,直接导入依赖到自己的工程中:

    6.png

    剩下的就是tinker官网的集成流程,这里不再复述了,具体在github中都有详细的集成方法,无非就是修改一些application的配置,增加一个applicationLike,把原本application中的初始化处理挪到applicationLike当中,这样的做法是可以让App支持修改application,applicationLike是通过反射的方法进行加载调用的,通过这种方法,就达到了热修复Application代码的效果。

    剩下的就是build gradle的集成,这里要把tinker的一些配置和变量添加到文件中,不多复述,直接copyGitHub上的就好
    这里要提一点的是,官方的build文件在生成apk的时候,会直接把apk文件放到bakApk目录下,如果编译次数过多的话,就会产生N多个文件,尤其是Release下,会生成apk、mapping、R三个文件。而且生成patch文件的时候,也需要修改gradle中的apk名称,十分繁琐,这里我稍加改造:

    /**
     * you can use assembleRelease to build you base apk
     * use tinkerPatchRelease -POLD_APK=  -PAPPLY_MAPPING=  -PAPPLY_RESOURCE= to build patch
     * add apk from the build/bakApk
     */
    ext {
        //for some reason, you may want to ignore tinkerBuild, such as instant run debug build?
        tinkerEnabled = true
    
        oldTime = "0806-18-21-02"
    
        oldAPKName = "release-${oldTime}"
    
        //only use for build all flavor, if not, just ignore this field
        tinkerBuildFlavorDirectory = "${bakPath}/${oldTime}"
    
        //for normal build
        //old apk file to build patch apk
        tinkerOldApkPath = "${bakPath}/${oldTime}/app-${oldAPKName}.apk"
        //proguard mapping file to build patch apk
        tinkerApplyMappingPath = "${bakPath}/${oldTime}/app-${oldAPKName}-mapping.txt"
        //resource R.txt to build patch apk, must input if there is resource changed
        tinkerApplyResourcePath = "${bakPath}/${oldTime}/app-${oldAPKName}-R.txt"
    }
    

    这里是增加了一个oldApkName和time,直接修改这里的time和name就好,替换原文件中的对应部分代码。

    另外一个就是在当前的基础上,增加一个时间目录,把apk文件、mapping文件、R文件统一放到一个目录下,便于管理和查看:

    
    /**
     * bak apk and mapping
     */
    android.applicationVariants.all { variant ->
        /**
         * task type, you want to bak
         */
        def taskName = variant.name
    
        tasks.all {
            if ("assemble${taskName.capitalize()}".equalsIgnoreCase(it.name)) {
    
                it.doLast {
                    copy {
                        def fileNamePrefix = "${project.name}-${variant.baseName}"
                        def newFileNamePrefix = hasFlavors ? "${date}/${fileNamePrefix}" : "${date}/${fileNamePrefix}-${date}"
    
                        def destPath = hasFlavors ? file("${bakPath}/${project.name}-${date}/${variant.flavorName}") : bakPath
                        from variant.outputs.first().outputFile
                        into destPath
                        rename { String fileName ->
                            fileName.replace("${fileNamePrefix}.apk", "${newFileNamePrefix}.apk")
                        }
    
                        from "${buildDir}/outputs/mapping/${variant.dirName}/mapping.txt"
                        into destPath
                        rename { String fileName ->
                            fileName.replace("mapping.txt", "${newFileNamePrefix}-mapping.txt")
                        }
    
                        from "${buildDir}/intermediates/symbols/${variant.dirName}/R.txt"
                        into destPath
                        rename { String fileName ->
                            fileName.replace("R.txt", "${newFileNamePrefix}-R.txt")
                        }
                    }
                }
            }
        }
    }
    
    

    就是在newFileNamePrefix这个参数后面,加上一个${date}即可。

    这样每次在运行的时候,build目录下就会生成以下文件(debug):

    7.png

    这里的文件对应的就是build.gradle中的oldApkPatch,在生成patch文件的过程中,也是根据这个路径来进行判断和处理的。而我们只需要修改oldApkPatch的路径就可以了,修改起来也很简单,我已经封装了oldName和oldTime,根据当前是否是正式环境或者测试环境来修改debug和Release版本,然后修改对应的时间就ok了。

    1.2 默认的工具、监听编写

    8.png

    tinker官方的sample中,有以上几个工具类,这都是对原本tinker的一些监听加载类做的二次封装,这里先拷贝过去就好,后面逐一进行分析。

    首先要关注TinkerManager这个类,这是封装的一个简易的工具类,对applicationLike进行的保存,并且包含tinker初始化的一些操作:

    主要看installTinker这个方法就好:

    9.png

    tinkerInstall是以build的方式进行加载的,这里我们先点进去看一下源码:

    10.png

    而在installTinker当中,创建的一些default监听器和加载器,其实都没有做过多的处理,这里其实只是想要表明,这些都可以进行自定义的,这样说明了tinker的复用性和可定制性都是比较强的。

    做完初始化的工作之后,其实tinker就已经接入到工程当中了,与bugly和tinker pathc不同,这里没有任何多余的操作和初始化内容,只是把tinker这个热更新工具集成进来,那如何使用呢?

    很简单,先生成一个apk文件包,也就是bakApk目录下要有一个已经生成好的版本目录,然后修改build.gradle文件中的路径指向这个apk包。

    11.png

    最后,根据当前是debug还是release版本执行对应的tinkerPatchDebug和tinkerPatchRelease,tinkerPatch就生成好了,生成好的文件路径如下图:

    12.png

    这个就是要执行的补丁包了,前提是要在生成之前修改好tinkerId,也就是build.gradle文件中的getSha()方法返回的字符串参数,原本tinker官方是把git当前节点的id作为版本id,这里我是写死了,大家随便写一个参数把base包和patch包做个区分就好。

    使用起来也很简单,把补丁包放到手机中可以读取到的目录,我这里是放到应用sd卡的cache目录下:

    File rootFile = getExternalCacheDir();
    if (rootFile != null && rootFile.exists()){
        mPath = rootFile.getAbsolutePath() + File.separatorChar;
    }
    ...
    File patchFile = new File(mPath, "patch_signed_7zip.apk");
    if (patchFile.exists()){
        TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), patchFile.getAbsolutePath());
    }
    

    执行以上代码其实就可以加载成功了,重启查看效果就OK,前提是要对patch版本的包做出一些改动。
    以上就是tinker的接入流程,其实结合网上大部分接入教程,基本都可以轻松的完成以上源码的接入,下面开始说一下对源码以及流程上的一些分析的分析。


    2. tinker源码执行流程的分析

    tinker 的使用流程分为三部分

    1. 生成差异包
    2. 加载差异补丁包
    3. 重新启动之后,通过tinkerLoader进行加载

    我会通过这三个步骤,逐一分析tinker 的加载过程。

    2.1 打包生成patch的流程

    调用tinkerPatchDebug和tinkerPatchRealse之后,是通过

    com.tencent.tinker.build.patch.Runner.tinkerPatch()
    

    来进行补丁加载的,文件在如下目录:

    13.png

    执行过程:

    14.png

    其实是通过apkDecoder来进行Path处理的,继续往下追踪,主要看一下他的patch方法:

    manifestDecoder.patch(oldFile, newFile);
    

    代码中首先对manifest文件进行了判断,主要是判断mainfest文件有没有做过修过,如果进行了修改,那么tinker就会抛出异常,是因为tinker自身并不支持修改AndroidManifest.xml文件,也就是不支持增加4大组件的。

    代码的下一步是一个文件写入的过程:

    Files.walkFileTree(mNewApkDir.toPath(), new ApkFilesVisitor(config, mNewApkDir.toPath(), mOldApkDir.toPath(), dexPatchDecoder, soPatchDecoder, resPatchDecoder));
    

    这里所有的比对操作都交给了ApkFilesVisitor,继续追踪,可以看到,ApkFilesVisitor文件中的所有操作都在 visitor方法中:

    15.png

    分析方法不难看出,所有的patch操作分为dex、so、res,分别交给了三个decoder去进行操作,然后我们逐一分析这些decoder。

    首先是dexDecoder,这些decoder都是在ApkDecoder的构造方法中进行初始化的,而dexDecoder是来源于UniqueDexDiffDecoder对象,该对象继承于DexDiffDecoder,而所有的patch操作也都是在该decoder类下的 patch(final File oldFile, final File newFile) 方法。

    该方法主要是对dex进行检测和比对,并保存新旧dex的对应关系到数组当中,执行完毕后,会走到onAllPatchesEnd()方法,最终会执行generatePatchInfoFile() 方法执行生成补丁文件。

    这里会执行生成一个DexPatchGenerator对象,负责整个新旧dex的比对工作,其中包含以下几种:

    1. private DexSectionDiffAlgorithm<StringData> stringDataSectionDiffAlg;
    2. private DexSectionDiffAlgorithm<Integer> typeIdSectionDiffAlg;
    3. private DexSectionDiffAlgorithm<ProtoId> protoIdSectionDiffAlg;
    4. private DexSectionDiffAlgorithm<FieldId> fieldIdSectionDiffAlg;
    5. private DexSectionDiffAlgorithm<MethodId> methodIdSectionDiffAlg;
    6. private DexSectionDiffAlgorithm<ClassDef> classDefSectionDiffAlg;
    7. private DexSectionDiffAlgorithm<TypeList> typeListSectionDiffAlg;
    8. private DexSectionDiffAlgorithm<AnnotationSetRefList> annotationSetRefListSectionDiffAlg;
    9. private DexSectionDiffAlgorithm<AnnotationSet> annotationSetSectionDiffAlg;
    10. private DexSectionDiffAlgorithm<ClassData> classDataSectionDiffAlg;
    11. private DexSectionDiffAlgorithm<Code> codeSectionDiffAlg;
    12. private DexSectionDiffAlgorithm<DebugInfoItem> debugInfoSectionDiffAlg;
    13. private DexSectionDiffAlgorithm<Annotation> annotationSectionDiffAlg;
    14. private DexSectionDiffAlgorithm<EncodedValue> encodedArraySectionDiffAlg;
    15. private DexSectionDiffAlgorithm<AnnotationsDirectory> annotationsDirectorySectionDiffAlg;

    这里涉及到了tinker的核心算法,以及dex格式的介绍,dex比对算法是二路归并,这里有一篇文章做了非常详细的解读:https://www.zybuluo.com/dodola/note/554061

    执行完executeAndSaveTo之后,比对后的差异也就以buffer的形式保存下来了。

    这里放个连接,里面有tinker对dex进行比对的详细描述,来自于官方:微信Tinker的一切都在这里,包括源码(一)

    接下来分析res的比对过程:

    资源的比对工作是交由ResDiffDecoder来完成的,我们点开该类下的patch方法来进行分析,核心的几个方法如下:

    File outputFile = getOutputPath(newFile).toFile();
    
    if (oldFile == null || !oldFile.exists()) {
        if (Utils.checkFileInPattern(config.mResIgnoreChangePattern, name)) {
            Logger.e("found add resource: " + name + " ,but it match ignore change pattern, just ignore!");
            return false;
        }
        FileOperation.copyFileUsingStream(newFile, outputFile);
        addedSet.add(name);
        writeResLog(newFile, oldFile, TypedValue.ADD);
        return true;
    }
    

    如果旧文件不存在,则直接添加到差异包中。

    //oldFile or newFile may be 0b length
    if (oldMd5 != null && oldMd5.equals(newMd5)) {
        return false;
    }
    

    MD5文件一致,则表示文件没有做出修改。

    如果文件发生变化,则是交给了dealWithModifyFile(name, newMd5, oldFile, newFile, outputFile);去进行处理。

    private boolean dealWithModifyFile(String name, String newMd5, File oldFile, File newFile, File outputFile) throws IOException {
            if (checkLargeModFile(newFile)) {
                if (!outputFile.getParentFile().exists()) {
                    outputFile.getParentFile().mkdirs();
                }
                BSDiff.bsdiff(oldFile, newFile, outputFile);
                //treat it as normal modify
                if (Utils.checkBsDiffFileSize(outputFile, newFile)) {
                    LargeModeInfo largeModeInfo = new LargeModeInfo();
                    largeModeInfo.path = newFile;
                    largeModeInfo.crc = FileOperation.getFileCrc32(newFile);
                    largeModeInfo.md5 = newMd5;
                    largeModifiedSet.add(name);
                    largeModifiedMap.put(name, largeModeInfo);
                    writeResLog(newFile, oldFile, TypedValue.LARGE_MOD);
                    return true;
                }
            }
            modifiedSet.add(name);
            FileOperation.copyFileUsingStream(newFile, outputFile);
            writeResLog(newFile, oldFile, TypedValue.MOD);
            return false;
        }
    

    核心就是BSDiff.bsdiff(oldFile, newFile, outputFile);

    这里利用bsdiff对文件进行二进制比对,目的是可以很好的控制差异文件的大小,这个就是一个增量更新的比对算法,这里不多做解释。

    最后在patch执行完毕后,会执行以下代码将所有检索出来的资源增删改的集合放到生成的meta文件中,而在tinker loader的时候,会读取补丁文件中的meta文件执行相对应的操作。

    //write meta file, write large modify first
    writeMetaFile(largeModifiedSet, TypedValue.LARGE_MOD);
    writeMetaFile(modifiedSet, TypedValue.MOD);
    writeMetaFile(addedSet, TypedValue.ADD);
    writeMetaFile(deletedSet, TypedValue.DEL);
    writeMetaFile(storedSet, TypedValue.STORED);
    

    文件在生成的补丁包中的assets下面的res_meta.txt,可自行解压补丁包查看。

    在然后是soDecoder。

    soDecoder其实就是BsDiffDecoder,与资源文件的比对和加载一样,通过bsDiff进行新旧文件对比,然后生成增量更新包,加载则是通过bsPatch来进行的。

    //new add file
    String newMd5 = MD5.getMD5(newFile);
    File bsDiffFile = getOutputPath(newFile).toFile();
    
    if (oldFile == null || !oldFile.exists()) {
        FileOperation.copyFileUsingStream(newFile, bsDiffFile);
        writeLogFiles(newFile, null, null, newMd5);
        return true;
    }
    
    //both file length is 0
    if (oldFile.length() == 0 && newFile.length() == 0) {
        return false;
    }
    if (oldFile.length() == 0 || newFile.length() == 0) {
        FileOperation.copyFileUsingStream(newFile, bsDiffFile);
        writeLogFiles(newFile, null, null, newMd5);
        return true;
    }
    
    //new add file
    String oldMd5 = MD5.getMD5(oldFile);
    
    if (oldMd5.equals(newMd5)) {
        return false;
    }
    
    if (!bsDiffFile.getParentFile().exists()) {
        bsDiffFile.getParentFile().mkdirs();
    }
    BSDiff.bsdiff(oldFile, newFile, bsDiffFile);
    
    if (Utils.checkBsDiffFileSize(bsDiffFile, newFile)) {
        writeLogFiles(newFile, oldFile, bsDiffFile, newMd5);
    } else {
        FileOperation.copyFileUsingStream(newFile, bsDiffFile);
        writeLogFiles(newFile, null, null, newMd5);
    }
    

    以上是so文件比对的核心部分,与res一致,首先也是判断有没有新增,其次比对文件的MD5是否一致,再然后是通过bsDiff进行比对生成差异化增量包。

    tinker更推荐的是把so库的加载交付给tinker去管理,这里贴出github wiki上的描述:

    不使用Hack的方式
    更新的Library库文件我们帮你保存在tinker下面的子目录下,但是我们并没有为你区分abi(部分手机判断不准确)。所以若想加载最新的库,你有两种方法,第一个是直接尝试去Tinker更新的库文件中加载,第二个参数是库文件相对安装包的路径。

    TinkerLoadLibrary.loadLibraryFromTinker(getApplicationContext(), "assets/x86", "libstlport_shared");
    

    但是我们更推荐的是,使用TinkerInstaller.loadLibrary接管你所有的库加载,它会自动先尝试去Tinker中的库文件中加载,但是需要注意的是当前这种方法只支持lib/armeabi目录下的库文件!

    //load lib/armeabi library
    TinkerLoadLibrary.loadArmLibrary(getApplicationContext(), "libstlport_shared");
    //load lib/armeabi-v7a library
    TinkerLoadLibrary.loadArmV7Library(getApplicationContext(), "libstlport_shared");
    

    若存在Tinker还没install之前调用加载补丁中的Library库,可使用TinkerApplicationHelper.java的接口

    //load lib/armeabi library
    TinkerApplicationHelper.loadArmLibrary(tinkerApplicationLike, "libstlport_shared");
    //load lib/armeabi-v7a library
    TinkerApplicationHelper.loadArmV7Library(tinkerApplicationLike, "libstlport_shared");
    

    若想对第三方代码的库文件更新,可先使用TinkerLoadLibrary.load*Library对第三方库做提前的加载!更多使用方法可参考MainActivity.java。

    查看了官方demo中的三个加载方式,使用起来正好对应的是:

    // #method 1, hack classloader library path
    TinkerLoadLibrary.installNavitveLibraryABI(getApplicationContext(), "armeabi");
    System.loadLibrary("stlport_shared");
    
    // #method 2, for lib/armeabi, just use TinkerInstaller.loadLibrary
    TinkerLoadLibrary.loadArmLibrary(getApplicationContext(), "stlport_shared");
    
    // #method 3, load tinker patch library directly
    TinkerInstaller.loadLibraryFromTinker(getApplicationContext(), "assets/x86", "stlport_shared");
    

    方法三这么写,参考bugly封装的TinkerManager:

    public static void loadLibraryFromTinker(Context context, String relativePath, String libname) {
        TinkerLoadLibrary.loadLibraryFromTinker(context, relativePath, libname);
    }
    

    经过了以上几个步骤之后,tinker会把所有比对后的记录,增量资源打包到补丁当中,接下来分析一下tinker的Patch的过程

    2.2 tinkerPatch的过程

    前面在接入部分我使用了tinker的加载补丁的方法:

    TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), patchFile.getAbsolutePath());
    

    就开始从onReceiveUpgradePatch进行代码的追踪。

    点进去这个方法:

    /**
     * new patch file to install, try install them with :patch process
     *
     * @param context
     * @param patchLocation
     */
    public static void onReceiveUpgradePatch(Context context, String patchLocation) {
        Tinker.with(context).getPatchListener().onPatchReceived(patchLocation);
    }
    

    可以看到这里获取了tinker对象的实例,通过app自身的application的上下文对象获取到,而后通过patch的listener的方法进行加载,这里listener只是一个接口,分析这里需要回到第一部分installTinker的时候添加的:

    //or you can just use DefaultLoadReporter
    LoadReporter loadReporter = new SampleLoadReporter(appLike.getApplication());
    //or you can just use DefaultPatchReporter
    PatchReporter patchReporter = new SamplePatchReporter(appLike.getApplication());
    //or you can just use DefaultPatchListener
    PatchListener patchListener = new SamplePatchListener(appLike.getApplication());
    

    这里主要看SamplePatchListener,追踪到DefaultPatchListener的onPatchReceived方法,这里就是开始进行patch操作的地方。

    首先通过patchCheck方法进行了一系列的校验工作,然后通过TinkerPatchService.runPatchService(context, path);运行了起了一个patchService,继续查看TinkerPatchService,可以看到以下两个核心的启动方法:

    private static void runPatchServiceByIntentService(Context context, String path) {
        TinkerLog.i(TAG, "run patch service by intent service.");
        Intent intent = new Intent(context, IntentServiceRunner.class);
        intent.putExtra(PATCH_PATH_EXTRA, path);
        intent.putExtra(RESULT_CLASS_EXTRA, resultServiceClass.getName());
        context.startService(intent);
    }
    
    @TargetApi(21)
    private static boolean runPatchServiceByJobScheduler(Context context, String path) {
        TinkerLog.i(TAG, "run patch service by job scheduler.");
        final JobInfo.Builder jobInfoBuilder = new JobInfo.Builder(
                1, new ComponentName(context, JobServiceRunner.class)
        );
        final PersistableBundle extras = new PersistableBundle();
        extras.putString(PATCH_PATH_EXTRA, path);
        extras.putString(RESULT_CLASS_EXTRA, resultServiceClass.getName());
        jobInfoBuilder.setExtras(extras);
        jobInfoBuilder.setOverrideDeadline(5);
        final JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
        if (jobScheduler == null) {
            TinkerLog.e(TAG, "jobScheduler is null.");
            return false;
        }
        return (jobScheduler.schedule(jobInfoBuilder.build()) == JobScheduler.RESULT_SUCCESS);
    }
    

    分别是android 21版本以上和以下两个方法,这是因为在android 5.0之后,为了对系统的耗电量和内存管理进行优化,Google官方要求对后台消耗资源的操作推荐放到JobScheduler中去执行。

    IntentServiceRunner是改Services的核心,这是一个异步的service,查看他的onHandleIntent,可以看到,所有的操作都在doApplyPatch(getApplicationContext(), intent);当中:

    @Override
    protected void onHandleIntent(@Nullable Intent intent) {
        increasingPriority();
        doApplyPatch(getApplicationContext(), intent);
    }
    

    方法中核心是:

    result = upgradePatchProcessor.tryPatch(context, path, patchResult);
    

    而这个upgradePatchProcessor也是在初始化的时候就传入的。

    //you can set your own upgrade patch if you need
    AbstractPatch upgradePatchProcessor = new UpgradePatch();
    

    我们查看UpgradePatch的tryPatch进行查看,方法很长,主要核心的部分是:

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

    这里分别对应的是dex的patch,so文件的patch,资源文件的patch。

    首先来看DexDiffPatchInternal.tryRecoverDexFiles,追踪代码可以看得出,经过一系列的操作校验之后,patchDexFile是执行patch操作的关键方法,而所有的处理交给了new DexPatchApplier(oldDexStream, patchFileStream).executeAndSaveTo(patchedDexFile),继续追踪下去,可以看到在生成补丁文件时候的熟悉代码,那就是那一系列dex的比对操作:

    // Secondly, run patch algorithms according to sections' dependencies.
    this.stringDataSectionPatchAlg = new StringDataSectionPatchAlgorithm(
            patchFile, oldDex, patchedDex, oldToPatchedIndexMap
    );
    this.typeIdSectionPatchAlg = new TypeIdSectionPatchAlgorithm(
            patchFile, oldDex, patchedDex, oldToPatchedIndexMap
    );
    this.protoIdSectionPatchAlg = new ProtoIdSectionPatchAlgorithm(
            patchFile, oldDex, patchedDex, oldToPatchedIndexMap
    );
    this.fieldIdSectionPatchAlg = new FieldIdSectionPatchAlgorithm(
            patchFile, oldDex, patchedDex, oldToPatchedIndexMap
    );
    this.methodIdSectionPatchAlg = new MethodIdSectionPatchAlgorithm(
            patchFile, oldDex, patchedDex, oldToPatchedIndexMap
    );
    this.classDefSectionPatchAlg = new ClassDefSectionPatchAlgorithm(
            patchFile, oldDex, patchedDex, oldToPatchedIndexMap
    );
    this.typeListSectionPatchAlg = new TypeListSectionPatchAlgorithm(
            patchFile, oldDex, patchedDex, oldToPatchedIndexMap
    );
    this.annotationSetRefListSectionPatchAlg = new AnnotationSetRefListSectionPatchAlgorithm(
            patchFile, oldDex, patchedDex, oldToPatchedIndexMap
    );
    this.annotationSetSectionPatchAlg = new AnnotationSetSectionPatchAlgorithm(
            patchFile, oldDex, patchedDex, oldToPatchedIndexMap
    );
    this.classDataSectionPatchAlg = new ClassDataSectionPatchAlgorithm(
            patchFile, oldDex, patchedDex, oldToPatchedIndexMap
    );
    this.codeSectionPatchAlg = new CodeSectionPatchAlgorithm(
            patchFile, oldDex, patchedDex, oldToPatchedIndexMap
    );
    this.debugInfoSectionPatchAlg = new DebugInfoItemSectionPatchAlgorithm(
            patchFile, oldDex, patchedDex, oldToPatchedIndexMap
    );
    this.annotationSectionPatchAlg = new AnnotationSectionPatchAlgorithm(
            patchFile, oldDex, patchedDex, oldToPatchedIndexMap
    );
    this.encodedArraySectionPatchAlg = new StaticValueSectionPatchAlgorithm(
            patchFile, oldDex, patchedDex, oldToPatchedIndexMap
    );
    this.annotationsDirectorySectionPatchAlg = new AnnotationsDirectorySectionPatchAlgorithm(
            patchFile, oldDex, patchedDex, oldToPatchedIndexMap
    );
    
    this.stringDataSectionPatchAlg.execute();
    this.typeIdSectionPatchAlg.execute();
    this.typeListSectionPatchAlg.execute();
    this.protoIdSectionPatchAlg.execute();
    this.fieldIdSectionPatchAlg.execute();
    this.methodIdSectionPatchAlg.execute();
    this.annotationSectionPatchAlg.execute();
    this.annotationSetSectionPatchAlg.execute();
    this.annotationSetRefListSectionPatchAlg.execute();
    this.annotationsDirectorySectionPatchAlg.execute();
    this.debugInfoSectionPatchAlg.execute();
    this.codeSectionPatchAlg.execute();
    this.classDataSectionPatchAlg.execute();
    this.encodedArraySectionPatchAlg.execute();
    this.classDefSectionPatchAlg.execute();
    

    截取其中一些片段,上面在生成的部分我提到过一篇分析tinker核心算法的文章,里面也描述了dex的结构,如下图:

    image

    这些比对的操作其实就是在对dex中的每个table进行的。

    执行完毕之后,依旧是通过二路归并的算法,生成经过了patch操作之后的dex文件,保存到本地目录,等待loader的时候使用。

    loader的后面说,继续看其他两个patch操作。

    res的patch操作是ResDiffPatchInternal.tryRecoverResourceFiles,追踪一下代码,可以看到,首先第一步就是先读取了之前生成的meta文件:

    String resourceMeta = checker.getMetaContentMap().get(RES_META_FILE);
    

    最后由extractResourceDiffInternals方法来进行补丁的合成,其实原理就是通过读取meta里面记录的每个资源对应的操作,来执行相关的增加、删除、修改的操作,最后将资源文件打包为apk文件,android自带的loader加载器其实也是支持.apk文件动态加载的。

    so文件的操作就更简单了,就是通过bsPatch进行一次patch操作,然后剩下的重载之类的方法前面也讲到了,和applicationLike的原理差不多,就是通过增加一层代理操作的方式,来达到托管的效果。

    patch完毕之后,tinker会在本地生成好可读取的补丁文件,便于再次启动的时候,进行加载。

    2.3 tinker loader的过程

    application 初始化的时候,就已经传入了com.tencent.tinker.loader.TinkerLoader,而tinkerloader也是通过TinkerApplication中反射进行加载的:

    private void loadTinker() {
        try {
            //reflect tinker loader, because loaderClass may be define by user!
            Class<?> tinkerLoadClass = Class.forName(loaderClassName, false, getClassLoader());
            Method loadMethod = tinkerLoadClass.getMethod(TINKER_LOADER_METHOD, TinkerApplication.class);
            Constructor<?> constructor = tinkerLoadClass.getConstructor();
            tinkerResultIntent = (Intent) loadMethod.invoke(constructor.newInstance(), this);
        } catch (Throwable e) {
            //has exception, put exception error code
            tinkerResultIntent = new Intent();
            ShareIntentUtil.setIntentReturnCode(tinkerResultIntent, ShareConstants.ERROR_LOAD_PATCH_UNKNOWN_EXCEPTION);
            tinkerResultIntent.putExtra(INTENT_PATCH_EXCEPTION, e);
        }
    }
    

    tinkerLoader的核心方法就是tryLoad,而该方法下又使用了tryLoadPatchFilesInternal,一个非常非常长的方法。

    其实逐一分析过后,无非就是三个loader:

    1. TinkerDexLoader
    2. TinkerSoLoader
    3. TinkerResourceLoader

    其余的一些方法中的操作大部分都是校验参数合法性,文件完整性的一些操作,可以略过。
    so的加载方式和原理前面已经说了,不在细说了,着重说一下dex和res的加载。

    TinkerDexLoader.loadTinkerJars是加载dex的核心方法,点进去又能看到一大部分校验的判断的,核心的加载内容是SystemClassLoaderAdder.installDexes(application, classLoader, optimizeDir, legalFiles);这段代码,查看installDexes可以看到tinker区分版本进行安装的操作:

    @SuppressLint("NewApi")
    public static void installDexes(Application application, PathClassLoader loader, File dexOptDir, List<File> files)
        throws Throwable {
        Log.i(TAG, "installDexes dexOptDir: " + dexOptDir.getAbsolutePath() + ", dex size:" + files.size());
    
        if (!files.isEmpty()) {
            files = createSortedAdditionalPathEntries(files);
            ClassLoader classLoader = loader;
            if (Build.VERSION.SDK_INT >= 24 && !checkIsProtectedApp(files)) {
                classLoader = AndroidNClassLoader.inject(loader, application);
            }
            //because in dalvik, if inner class is not the same classloader with it wrapper class.
            //it won't fail at dex2opt
            if (Build.VERSION.SDK_INT >= 23) {
                V23.install(classLoader, files, dexOptDir);
            } else if (Build.VERSION.SDK_INT >= 19) {
                V19.install(classLoader, files, dexOptDir);
            } else if (Build.VERSION.SDK_INT >= 14) {
                V14.install(classLoader, files, dexOptDir);
            } else {
                V4.install(classLoader, files, dexOptDir);
            }
            //install done
            sPatchDexCount = files.size();
            Log.i(TAG, "after loaded classloader: " + classLoader + ", dex size:" + sPatchDexCount);
    
            if (!checkDexInstall(classLoader)) {
                //reset patch dex
                SystemClassLoaderAdder.uninstallPatchDex(classLoader);
                throw new TinkerRuntimeException(ShareConstants.CHECK_DEX_INSTALL_FAIL);
            }
        }
    }
    

    这里之所以要区分版本,tinker官方也描述了一些android版本上的坑,前面分享过的连接中有详细的描述,这里随便点开一个看一下,我打开的v23的:

    private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
                                        File optimizedDirectory)
        throws IllegalArgumentException, IllegalAccessException,
        NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
        /* The patched class loader is expected to be a descendant of
         * dalvik.system.BaseDexClassLoader. We modify its
         * dalvik.system.DexPathList pathList field to append additional DEX
         * file entries.
         */
        Field pathListField = ShareReflectUtil.findField(loader, "pathList");
        Object dexPathList = pathListField.get(loader);
        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
        ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", makePathElements(dexPathList,
            new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
            suppressedExceptions));
        if (suppressedExceptions.size() > 0) {
            for (IOException e : suppressedExceptions) {
                Log.w(TAG, "Exception in makePathElement", e);
                throw e;
            }
    
        }
    }
    

    通过这个方法可以看出,tinker也是通过反射,获取到系统ClassLoader的dexElements数组,并把需要修改的dex文件插入到了数组当中的最前端。

    这里就是tinker整个代码热更新的原理,就是把合并过后的dex文件,插入到Elements数组的前端,因为android的类加载器在加载dex的时候,会按照数组的顺序查找,如果在下标靠前的位置查找到了,就不继续向下寻找了,所以也就起到了热更新的作用。

    继续看Res的加载。

    TinkerResourceLoader.loadTinkerResources。

    方法中的核心部分是:TinkerResourcePatcher.monkeyPatchExistingResources(application, resourceString);

    /**
     * @param context
     * @param externalResourceFile
     * @throws Throwable
     */
    public static void monkeyPatchExistingResources(Context context, String externalResourceFile) throws Throwable {
        if (externalResourceFile == null) {
            return;
        }
    
        final ApplicationInfo appInfo = context.getApplicationInfo();
    
        final Field[] packagesFields;
        if (Build.VERSION.SDK_INT < 27) {
            packagesFields = new Field[]{packagesFiled, resourcePackagesFiled};
        } else {
            packagesFields = new Field[]{packagesFiled};
        }
        for (Field field : packagesFields) {
            final Object value = field.get(currentActivityThread);
    
            for (Map.Entry<String, WeakReference<?>> entry
                    : ((Map<String, WeakReference<?>>) value).entrySet()) {
                final Object loadedApk = entry.getValue().get();
                if (loadedApk == null) {
                    continue;
                }
                final String resDirPath = (String) resDir.get(loadedApk);
                if (appInfo.sourceDir.equals(resDirPath)) {
                    resDir.set(loadedApk, externalResourceFile);
                }
            }
        }
    
        // Create a new AssetManager instance and point it to the resources installed under
        if (((Integer) addAssetPathMethod.invoke(newAssetManager, externalResourceFile)) == 0) {
            throw new IllegalStateException("Could not create new AssetManager");
        }
    
        // Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm
        // in L, so we do it unconditionally.
        if (stringBlocksField != null && ensureStringBlocksMethod != null) {
            stringBlocksField.set(newAssetManager, null);
            ensureStringBlocksMethod.invoke(newAssetManager);
        }
    
        for (WeakReference<Resources> wr : references) {
            final Resources resources = wr.get();
            if (resources == null) {
                continue;
            }
            // Set the AssetManager of the Resources instance to our brand new one
            try {
                //pre-N
                assetsFiled.set(resources, newAssetManager);
            } catch (Throwable ignore) {
                // N
                final Object resourceImpl = resourcesImplFiled.get(resources);
                // for Huawei HwResourcesImpl
                final Field implAssets = findField(resourceImpl, "mAssets");
                implAssets.set(resourceImpl, newAssetManager);
            }
    
            clearPreloadTypedArrayIssue(resources);
    
            resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics());
        }
    
        // Handle issues caused by WebView on Android N.
        // Issue: On Android N, if an activity contains a webview, when screen rotates
        // our resource patch may lost effects.
        // for 5.x/6.x, we found Couldn't expand RemoteView for StatusBarNotification Exception
        if (Build.VERSION.SDK_INT >= 24) {
            try {
                if (publicSourceDirField != null) {
                    publicSourceDirField.set(context.getApplicationInfo(), externalResourceFile);
                }
            } catch (Throwable ignore) {
            }
        }
    
        if (!checkResUpdate(context)) {
            throw new TinkerRuntimeException(ShareConstants.CHECK_RES_INSTALL_FAIL);
        }
    }
    

    这里分析不难看出,其实就是通过反射的方法,替换掉系统的AssetManager,也就是mAssets这个变量,而新的NewAssetManager指向的resource是新的资源路径,这样在系统调用mAssets进行加载资源的时候,使用的就是热更新后的资源了。

    over~

    相关文章

      网友评论

        本文标题:Tinker热更新源码接入以及分析

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