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

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~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容