Tinker热更新源码接入以及分析
介绍:tinker是一款由腾讯微信团队提供的热更新软件,有着微信庞大的用户基础,每一版tinker都会使用超过200个版本的微信进行兼容性和稳定性的测试,并且与手机厂商有着深度的合作,每一版tinker发布之前,都会先经过手机厂商的测试,并且微信使用的也是tinker的开源版本。相比较于其他的热更新软件,tinker更具有可靠性。
1.0 以源码的方式接入Tinker
Tinker GitHub 连接: https://github.com/Tencent/tinker
建议阅读前先看一遍官网的github,进行一下了解。
下载源码zip包进行解压,我们要使用到的就是Tinker的以下几个插件包:
tinrd-party:这个是tinker所使用到的一些工具类插件,例如tinker的核心bsdiff、dex文件处理工具、解压zip文件之类的。
tinker-android:这个就是tinker接入到android工程中之后的主要功能模块,包含了patch以及loader的部分。
tinker-build:tinker对本地apk进行分析打包,生成patch文件的主要包。
tinker-commons:公共包,放了一些公共的patch算法的东西。
1.1 集成
首先第一步将这四个model导入到当前的工程中,导入之后肯定会出现编译错误,我这里不罗列错误,就把解决方法罗列一下:
导入项目成功之后,肯定会报找不到grandle下面的这四个文件,如上图,从tinker源码目录把这四个文件拷贝过来就行,其中有一个是push的gradle,在这些model中都有引用,下面这个直接删除就好,如下图:
解决完gradle文件的错误之后,是一些model中的compile引用依赖报错,这是因为android gradle版本造成的,tinker默认使用的2.2版本,而在gradle3中,推荐使用的是implementation和api,具体原由不赘述了,
我是采用的直接改成api,这里tinker官方sample里面有更好的集成方式:
判断gradle的版本,而后使用不同的集成依赖的方式。
到这一步,应该源码包就没有什么问题了,直接导入依赖到自己的工程中:
剩下的就是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):
这里的文件对应的就是build.gradle中的oldApkPatch,在生成patch文件的过程中,也是根据这个路径来进行判断和处理的。而我们只需要修改oldApkPatch的路径就可以了,修改起来也很简单,我已经封装了oldName和oldTime,根据当前是否是正式环境或者测试环境来修改debug和Release版本,然后修改对应的时间就ok了。
1.2 默认的工具、监听编写
tinker官方的sample中,有以上几个工具类,这都是对原本tinker的一些监听加载类做的二次封装,这里先拷贝过去就好,后面逐一进行分析。
首先要关注TinkerManager这个类,这是封装的一个简易的工具类,对applicationLike进行的保存,并且包含tinker初始化的一些操作:
主要看installTinker这个方法就好:
tinkerInstall是以build的方式进行加载的,这里我们先点进去看一下源码:
而在installTinker当中,创建的一些default监听器和加载器,其实都没有做过多的处理,这里其实只是想要表明,这些都可以进行自定义的,这样说明了tinker的复用性和可定制性都是比较强的。
做完初始化的工作之后,其实tinker就已经接入到工程当中了,与bugly和tinker pathc不同,这里没有任何多余的操作和初始化内容,只是把tinker这个热更新工具集成进来,那如何使用呢?
很简单,先生成一个apk文件包,也就是bakApk目录下要有一个已经生成好的版本目录,然后修改build.gradle文件中的路径指向这个apk包。
最后,根据当前是debug还是release版本执行对应的tinkerPatchDebug和tinkerPatchRelease,tinkerPatch就生成好了,生成好的文件路径如下图:
这个就是要执行的补丁包了,前提是要在生成之前修改好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 的使用流程分为三部分
- 生成差异包
- 加载差异补丁包
- 重新启动之后,通过tinkerLoader进行加载
我会通过这三个步骤,逐一分析tinker 的加载过程。
2.1 打包生成patch的流程
调用tinkerPatchDebug和tinkerPatchRealse之后,是通过
com.tencent.tinker.build.patch.Runner.tinkerPatch()
来进行补丁加载的,文件在如下目录:
执行过程:
其实是通过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方法中:
分析方法不难看出,所有的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的比对工作,其中包含以下几种:
- private DexSectionDiffAlgorithm
stringDataSectionDiffAlg; - private DexSectionDiffAlgorithm
typeIdSectionDiffAlg; - private DexSectionDiffAlgorithm
protoIdSectionDiffAlg; - private DexSectionDiffAlgorithm
fieldIdSectionDiffAlg; - private DexSectionDiffAlgorithm
methodIdSectionDiffAlg; - private DexSectionDiffAlgorithm
classDefSectionDiffAlg; - private DexSectionDiffAlgorithm
typeListSectionDiffAlg; - private DexSectionDiffAlgorithm
annotationSetRefListSectionDiffAlg; - private DexSectionDiffAlgorithm
annotationSetSectionDiffAlg; - private DexSectionDiffAlgorithm
classDataSectionDiffAlg; - private DexSectionDiffAlgorithm
codeSectionDiffAlg;
- private DexSectionDiffAlgorithm
debugInfoSectionDiffAlg; - private DexSectionDiffAlgorithm
annotationSectionDiffAlg; - private DexSectionDiffAlgorithm
encodedArraySectionDiffAlg; - private DexSectionDiffAlgorithm
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的结构,如下图:
这些比对的操作其实就是在对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:
- TinkerDexLoader
- TinkerSoLoader
- 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 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 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 suppressedExceptions = new ArrayList();
ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", makePathElements(dexPathList,
new ArrayList(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> entry
: ((Map>) 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 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~