《Android tinker热修复——实战接入项目》看了该文章的读者应该懂得如何使用tinker接入自己的项目了,但是作为开发人员,会使用人家的框架还远远不够,我们需要学习别人的设计思想和原理,来提高自己。
在会使用了tinker的基础上,接下来我们深入学习一下tinker到底是如何工作,如何加载合成补丁的,如何修复bug的。由于tinker热修复框架比较强大,而且原理和思想比较深,所以一篇文章去探索是远远不够的,我们需要一个步骤一个步骤的解析,而该片文章主要是探析tinker的补丁加载合成。
我们看下tinker的工作流程:
接下来我们分析一下源码的实现过程,这里我们挑了合成patch的流程。
开始补丁合成
下载完成补丁之后,将要调用TinkerInstaller的onReceiveUpgradePatch方法。
安装合成新的补丁包,并且启动补丁进程。
Tinker.with(context):
这里主要是初始化tinker的基本配置,并且使用同步机制来初始化配置。
new Builder(context).build():
初始化的配置,我都已经在代码中注释好了。在build()方法的最后,new了Tinker对象,而在Tinker构造方法里面主要是将刚刚初始化的默认配置,缓存到Tinker里面。
我们看回onReceiveUpgradePatch方法里面调用的getPatchListener,而该方法主要是返回刚刚在build里面初始化的DefaultPatchListener对象。
DefaultPatchListener就是检查修复包的类,最后调用该类的onPatchReceived,该方法开始检查修复包,检查完成之后就开始合成补丁包。
在onPatchReceived里面调用patchCheck方法检查修复包
/**
* when we receive a patch, what would we do?
* you can overwrite it
*
* @param path 补丁路径
* @return
*/
@Override
public int onPatchReceived(String path) {
File patchFile = new File(path);
int returnCode = patchCheck(path, SharePatchFileUtil.getMD5(patchFile));
//一切准备好之后,调用runPatchService方法,启动补丁的Service服务
if (returnCode == ShareConstants.ERROR_PATCH_OK) {
TinkerPatchService.runPatchService(context, path);
} else {
Tinker.with(context).getLoadReporter().onLoadPatchListenerReceiveFail(new File(path), returnCode);
}
return returnCode;
}
/**
*
* @param path 补丁路径
* @param patchMd5 //补丁文件的md5值
* @return
*/
protected int patchCheck(String path, String patchMd5) {
Tinker manager = Tinker.with(context);
//check SharePreferences also
//检查是否开启补丁修复
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 service can not send request
//补丁服务不能发送请求
if (manager.isPatchProcess()) {
return ShareConstants.ERROR_PATCH_INSERVICE;
}
//if the patch service is running, pending
//如果补丁服务的servicec正在运行,就等待
if (TinkerServiceInternals.isTinkerPatchServiceRunning(context)) {
return ShareConstants.ERROR_PATCH_RUNNING;
}
//检查JIT
if (ShareTinkerInternals.isVmJit()) {
return ShareConstants.ERROR_PATCH_JIT;
}
//获取tinker对象
Tinker tinker = Tinker.with(context);
//tinker加载完成
if (tinker.isTinkerLoaded()) {
//加载完成结果
TinkerLoadResult tinkerLoadResult = tinker.getTinkerLoadResultIfPresent();
if (tinkerLoadResult != null && !tinkerLoadResult.useInterpretMode) {
String currentVersion = tinkerLoadResult.currentVersion;
//补丁没有变和当前版本相同
if (patchMd5.equals(currentVersion)) {
return ShareConstants.ERROR_PATCH_ALREADY_APPLY;
}
}
}
if (!UpgradePatchRetry.getInstance(context).onPatchListenerCheck(patchMd5)) {
return ShareConstants.ERROR_PATCH_RETRY_COUNT_LIMIT;
}
return ShareConstants.ERROR_PATCH_OK;
}
复制代码
经过一系列的检查,包括检查是否开启补丁修复、补丁是否存在和补丁服务是否正在运行等等,最后返回ERROR_PATCH_OK
表示补丁验证通过。
补丁的检验通过之后会调用TinkerPatchService.runPatchService来启动合成补丁的服务。否则调用DefaultLoadReporter的onLoadPatchListenerReceiveFail方法,报告加载补丁包失败。
//一切准备好之后,调用runPatchService方法,启动补丁的Service服务
if (returnCode == ShareConstants.ERROR_PATCH_OK) {
TinkerPatchService.runPatchService(context, path);
} else {
Tinker.with(context).getLoadReporter()
.onLoadPatchListenerReceiveFail
(new File(path), returnCode);
}
复制代码
我们主要看TinkerPatchService的runPatchService方法,启动服务。
该方法接受的参数就是将补丁的路径和上下文传过去,启动TinkerPatchService服务,而TinkerPatchService该服务是属于:patch
进程的。TinkerPatchService继承IntentService,需要重写onHandleIntent方法,该方法是运行在子线程当中,可以做一些耗时任务,任务完成之后会自己结束掉服务。
启动服务之后,我们主要看TinkerPatchService的onHandleIntent方法。
@Override
protected void onHandleIntent(Intent intent) {
final Context context = getApplicationContext();
//获取tinker对象
Tinker tinker = Tinker.with(context);
//调用DefaultPatchReporter的onPatchServiceStart方法
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);
//获取从设备boot后经历的时间值。
long begin = SystemClock.elapsedRealtime();
boolean result;
long cost;
Throwable e = null;
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;
tinker.getPatchReporter().
onPatchResult(patchFile, result, cost);
patchResult.isSuccess = result;
patchResult.rawPatchFilePath = path;
patchResult.costTime = cost;
patchResult.e = e;
//关闭patch进程,如果更新成功就要删掉rawPatchFilePath
AbstractResultService.runResultService(
context,
patchResult,
getPatchResultExtra(intent));
}
复制代码
在onHandleIntent方法里面,首先会调用DefaultPatchReporter的onPatchServiceStart方法,而DefaultPatchReporter主要是打修复包过程中的报告类。看下onPatchServiceStart方法做了那些事情。
主要做的工作就是报告TinkerPatchService开始时的一些工作。
继续往下看,最后调用UpgradePatchRetry的onPatchServiceStart方法。
/**
* 启动服务要做的事情
* 包括把文件搬到/data/data/包名下
* @param intent
*/
public void onPatchServiceStart(Intent intent) {
if (!isRetryEnable) {
TinkerLog.w(TAG,
"onPatchServiceStart retry disabled, just return");
return;
}
if (intent == null) {
TinkerLog.e(TAG,
"onPatchServiceStart intent is null, just return");
return;
}
String path = TinkerPatchService.getPatchPathExtra(intent);
if (path == null) {
TinkerLog.w(TAG,
"onPatchServiceStart patch path is null, just return");
return;
}
RetryInfo retryInfo;
File patchFile = new File(path);
String patchMd5 = SharePatchFileUtil.getMD5(patchFile);
if (patchMd5 == null) {
TinkerLog.w(TAG,
"onPatchServiceStart patch md5 is null, just return");
return;
}
if (retryInfoFile.exists()) {
retryInfo = RetryInfo.readRetryProperty(retryInfoFile);
if (retryInfo.md5 == null ||
retryInfo.times == null ||
!patchMd5.equals(retryInfo.md5)) {
copyToTempFile(patchFile);
retryInfo.md5 = patchMd5;
retryInfo.times = "1";
} else {
int nowTimes = Integer.parseInt(retryInfo.times);
if (nowTimes >= maxRetryCount) {
SharePatchFileUtil.safeDeleteFile(tempPatchFile);
TinkerLog.w(TAG,
"onPatchServiceStart retry more than max count" +
", delete retry info file!");
return;
} else {
retryInfo.times = String.valueOf(nowTimes + 1);
}
}
} else {
copyToTempFile(patchFile);
retryInfo = new RetryInfo(patchMd5, "1");
}
//重写属性
RetryInfo.writeRetryProperty(retryInfoFile, retryInfo);
}
复制代码
该方法也经过一些列的判断,往下看,我们看到copyToTempFile(patchFile)这个方法。
方法内调用SharePatchFileUtil.copyFileUsingStream方法。
/**
* 将补丁文件copy到dest文件下
* @param source
* @param dest
* @throws IOException
*/
public static void copyFileUsingStream(File source,
File dest)
throws IOException {
if (!SharePatchFileUtil.isLegalFile(source) ||
dest == null) {
return;
}
if (source.getAbsolutePath().
equals(dest.getAbsolutePath())) {
return;
}
FileInputStream is = null;
FileOutputStream os = null;
File parent = dest.getParentFile();
if (parent != null && (!parent.exists())) {
parent.mkdirs();
}
try {
is = new FileInputStream(source);
os = new FileOutputStream(dest, false);
byte[] buffer = new byte[ShareConstants.
BUFFER_SIZE];
int length;
while ((length = is.read(buffer)) > 0) {
os.write(buffer, 0, length);
}
} finally {
closeQuietly(is);
closeQuietly(os);
}
}
复制代码
该方法就是将补丁文件copy到dest文件,在UpgradePatchRetry创建对象的时候就初始化了tempPatchFile文件(/data/data/包名/tinker_temp/temp.apk)。
看回onPatchServiceStart方法,最后调用writeRetryProperty方法。
更新RetryInfo对象的属性,存储着补丁的md5和time。
onPatchServiceStart:该方法主要的作用就是将补丁文件拷贝一份到(/data/data/包名/tinker_temp/temp.apk)temp.apk文件,然后更新补丁文件的md5和time并且存储起来。
继续看onHandleIntent方法,经过intent判断,通过onHandleIntent方法拿到path之后,然后通过increasingPriority()方法,将服务设置到前台来,为了就是让该无法不被系统杀死。
service设置到前台
startForeground(notificationId, notification)
startForeground(notificationId, notification):后台服务置于前台,就像音乐播放器的播放服务一样,不会被系统杀死。如果Build.VERSION.SDK_INT > 18,开启一个InnerService降低被杀死的概率。
将service调到前台之后,接着调用UpgradePatch的tryPatch方法,该方法就是合成补丁包的过程。
@Override
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;
TinkerLog.i(TAG,
"UpgradePatch tryPatch:patchMd5:%s", 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;
}
复制代码
微信团队写代码是非常严谨的,在tryPatch方法中,需要经过一系列的判断(包括判断是否开启热修复,是否存在补丁包,md5是否为null等)之后,然后调用补丁合成的方法。
在此之前new了一个名为destPatchFile的文件,如果之前没有存在该文件,就需要将补丁复制写入,合成的时候需要用destPatchFile文件,因为如果使用原来的补丁文件,在合成的过程中,用户有可能删除补丁,所以为了安全需要使用destPatchFile文件来进行合成。
注意:
1、补丁包必须拷贝到/data/data/包名/
目录下,通过从下载目录文件通过流读出写入到该目录下,因为修复替换patch需要在/data/data/包名/
目录下进行。
2、在合成的时候,分别使用DexDiff合成dex、BsDiff合成library和ResDiff合成resource。
最后一部就是拷贝SharePatchInfo到PatchInfoFile中,使用SharePatchInfo的rewritePatchInfoFileWithLock方法。
执行完补丁的合成之后,在TinkerPatchService的onHandleIntent方法中,会调用AbstractResultService的runResultService方法,而runResultService方法启动的service就是我们在调用tinker.install方法传入的service。在TinkerSample中传入的是SampleResultService,而SampleResultService的onPatchResult方法,主要做的就是:
1、调用killTinkerPatchServiceProcess关闭patchService的进程
2、调用deleteRawPatchFile删掉补丁文件。
以上就是补丁合成的过程,但是没有深入到合成算法的分析,简单的合成流程分析,算法还需要时间慢慢啃。
参考文章:《微信热补丁Tinker – 补丁流程》