# Tinker学习计划(2)-Tinker的原理一
前言
在 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.
解决该问题:只需要修改以下两个地方:
-
修改 /gradle/gradle-wrapper.properties 文件:
distributionUrl=https://services.gradle.org/distributions/gradle-3.3-all.zip
修改为:
distributionUrl=https://services.gradle.org/distributions/gradle-2.14.1-all.zip
-
工程根目录 build.gradle 的gradle插件依赖修改版本,如下:
classpath 'com.android.tools.build:gradle:2.2.0'
-
其他的估计就是一些路径问题,出现的时候看着改下就可以了。
可能有的童鞋还是不太了解,索性简单花了一个图来表示各个库的依赖关系:
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做了哪些事情。下一节我们继续分析。