Tinker源码解析系列(二)—补丁合成与加载
上次我们分析了Tinker中关于Application的代理机制,这次我们来分析一下Tinker是如何进行补丁的合成与加载的。
这里也附上之前的文章传送门:Android热修复原理探索与实践
Tinker源码解析系列(一)—Application代理机制
阅读本文可能需要花费10分钟。
以下所有对源码的分析均基于Tinker 1.7.7 版本,且由于源码过长,均只贴出关键部分
Tinker中比较关键的几个类
在开始分析补丁合成和加载过程中,我们先来看看在Tinker中比较重要的几个类。
-
PatchListener
Patch监听者,负责执行获取到补丁包后的后续操作 -
LoadReporter
Load记录者,负责记录加载补丁包过程的状态信息 -
PatchReporter
Patch记录者,负责记录合成补丁过程的状态信息 -
AbstractPatch
Patch执行者,负责执行补丁的合成
其中,PatchListener
,LoadReporter
,PatchReporter
均为接口,AbstractPatch
为抽象类,在Tinker中他们都有默认的实现类,对应关系如下: -
PatchListener
默认实现类为DefaultPatchListener
-
LoadReporter
默认实现类为DefaultLoadReporter
-
PatchReporter
默认实现类为DefaultPatchReporter
-
AbstractPatch
默认实现类为UpgradePatch
Tinker的成员变量中包含了PatchListener
, LoadReporter
,PatchReporter
public class Tinker {
...
final PatchListener listener;
final LoadReporter loadReporter;
final PatchReporter patchReporter;
...
}
在Tinker中采用了Builder模式进行构造,我们直接看一下源码是怎么对他们进行赋值的
public static class Builder {
public Tinker build() {
...
if (loadReporter == null) {
loadReporter = new DefaultLoadReporter(context);
}
if (patchReporter == null) {
patchReporter = new DefaultPatchReporter(context);
}
if (listener == null) {
listener = new DefaultPatchListener(context);
}
...
return new Tinker(context, status, loadReporter, patchReporter, listener, patchDirectory,
patchInfoFile, patchInfoLockFile, mainProcess, patchProcess, tinkerLoadVerifyFlag);
}
}
看在这里,为什么我们没有发现AbstractPatch
的影子呀?其实它是在install
方法中进行设置的,我们直接看一下源码,
public class Tinker {
...
public void install(Intent intentResult) {
install(intentResult, DefaultTinkerResultService.class, new UpgradePatch());
}
...
}
所以说Tinker提供了强大的自定义能力,我们完全能够去继承这几个类从而实现我们一些自定义的需求,好了不扯远了,这里我们大概清楚了这几个类的作用,接下来我们就进入正题
补丁的合成
在Tinker的官方Demo中,补丁合成调用的方法是
TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed_7zip.apk");
我们点击该方法进去,
public class TinkerInstaller {
public static void onReceiveUpgradePatch(Context context, String patchLocation) {
Tinker.with(context).getPatchListener().onPatchReceived(patchLocation);
}
}
可以看到,实际上是调用了PatchListener的onPatchReceived
方法,我们前面说过,在Tinker中,如果我们没有自定义PatchListener,那么默认的PatchListener就是DefaultPatchListener
,这里我们直接去到DefaultPatchListener
查看它的onPatchReceived
方法
public int onPatchReceived(String path) {
//对补丁路径进行校验,进行路径是否存在等等进行判断
int returnCode = patchCheck(path);
if (returnCode == ShareConstants.ERROR_PATCH_OK) {
//校验成功,开启Patch服务
TinkerPatchService.runPatchService(context, path);
} else {
//校验失败,交给LoadReporter加载记录者进行状态记录
Tinker.with(context).getLoadReporter().onLoadPatchListenerReceiveFail(new File(path), returnCode);
}
return returnCode;
}
可以看到,补丁文件检验成功后会通过TinkerPatchService.runPatchService(context, path)
运行TinkerPatchService,代码很简单
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);
}
}
就是将补丁路径和resultServiceClass通过Intent传递,其中resultServiceClass是一个合成补丁结束后执行的Service,它的赋值也在Tinker的install
方法中执行,我们后面会讲到它
TinkerPatchService继承了IntentService,这里我们直接看看它的onHandleIntent
方法,
protected void onHandleIntent(Intent intent) {
final Context context = getApplicationContext();
Tinker tinker = Tinker.with(context);
//调用PatchReporter记录Patch开始的状态
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;
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;
//合成补丁结束,运行合成补丁后的Service
AbstractResultService.runResultService(context, patchResult, getPatchResultExtra(intent));
}
别看代码长,其实最核心的就是upgradePatchProcessor.tryPatch(context, path, patchResult)
而已,其中upgradePatchProcessor
其实就是AbstractPatch
的实现者,在Tinker的install
方法中被赋值,默认为UpgradePatch
,这我们前面已经分析过了,那么,合成补丁的过程,实际上就是在UpgradePatch
的tryPatch
方法之中了
这里穿插一个比较有意思的细节,就是上面的 increasingPriority()
方法,看方法名的意思大概是提升权重的意思,我们来看一下代码
private void increasingPriority() {
try {
Notification notification = new Notification();
if (Build.VERSION.SDK_INT < 18) {
startForeground(notificationId, notification);
} else {
startForeground(notificationId, notification);
// start InnerService
startService(new Intent(this, InnerService.class));
}
} catch (Throwable e) {
TinkerLog.i(TAG, "try to increase patch process priority error:" + e);
}
}
现在大家肯定对这段代码不陌生,就是利用系统漏洞进行保活~ 好了回归正题,我们接着看UpgradePatch
的tryPatch
方法
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;
}
//it is a new patch, so we should not find a exist
SharePatchInfo oldInfo = manager.getTinkerLoadResultIfPresent().patchInfo;
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;
SharePatchInfo newInfo;
//already have patch
if (oldInfo != null) {
if (oldInfo.oldVersion == null || oldInfo.newVersion == 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;
}
newInfo = new SharePatchInfo(oldInfo.oldVersion, patchMd5, Build.FINGERPRINT);
} else {
newInfo = new SharePatchInfo("", patchMd5, Build.FINGERPRINT);
}
//check ok, we can real recover a new patch
final String patchDirectory = manager.getPatchDirectory().getAbsolutePath();
TinkerLog.i(TAG, "UpgradePatch tryPatch:patchMd5:%s", patchMd5);
final String patchName = SharePatchFileUtil.getPatchVersionDirectory(patchMd5);
final String patchVersionDirectory = patchDirectory + "/" + patchName;
TinkerLog.i(TAG, "UpgradePatch tryPatch:patchVersionDirectory:%s", patchVersionDirectory);
//it is a new patch, we first delete if there is any files
//don't delete dir for faster retry
// SharePatchFileUtil.deleteDir(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
// just warn
if (!DexDiffPatchInternal.waitDexOptFile()) {
TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, check dex opt file failed");
}
final File patchInfoFile = manager.getPatchInfoFile();
if (!SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, newInfo, SharePatchFileUtil.getPatchInfoLockFile(patchDirectory))) {
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;
}
方法很长,但大部分代码都是在进行一些安全性的检验,例如检查tinkerId,签名,md5值等等
为了方便大家,我直接列出几个核心代码
-
DexDiffPatchInternal.tryRecoverDexFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)
进行dex文件的合成 -
BsDiffPatchInternal.tryRecoverLibraryFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)
进行.so库的合成 -
ResDiffPatchInternal.tryRecoverResourceFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)
进行资源文件的合成
这里由于本文主要是分析补丁合成和加载的过程,所以这里不展开合成算法细节的分析了(其实是我算法菜。。就不班门弄斧了),对合成算法diff细节感兴趣的同学可以查看这篇博客Dexdiff算法解析
ok,到这里基本上补丁包的合成就结束了,还记得上面说的那个在补丁合成结束后运行的Service吗?我们回头看看它
//合成补丁结束,运行合成补丁后的Service
AbstractResultService.runResultService(context, patchResult, getPatchResultExtra(intent));
public static void runResultService(Context context, PatchResult result, String resultServiceClass) {
if (resultServiceClass == null) {
throw new TinkerRuntimeException("resultServiceClass is null.");
}
try {
Intent intent = new Intent();
intent.setClassName(context, resultServiceClass);
intent.putExtra(RESULT_EXTRA, result);
context.startService(intent);
} catch (Throwable throwable) {
TinkerLog.e(TAG, "run result service fail, exception:" + throwable);
}
}
代码很简单,就是开启一个service,resultServiceClass的赋值过程在Tinker的install
方法中,
public void install(Intent intentResult) {
install(intentResult, DefaultTinkerResultService.class, new UpgradePatch());
}
DefaultTinkerResultService便是默认的resultServiceClass,它的代码其实也很简单
public class DefaultTinkerResultService extends AbstractResultService {
private static final String TAG = "Tinker.DefaultTinkerResultService";
/**
* we may want to use the new patch just now!!
*
* @param result
*/
@Override
public void onPatchResult(PatchResult result) {
if (result == null) {
TinkerLog.e(TAG, "DefaultTinkerResultService received null result!!!!");
return;
}
TinkerLog.i(TAG, "DefaultTinkerResultService received a result:%s ", result.toString());
//first, we want to kill the recover process
TinkerServiceInternals.killTinkerPatchServiceProcess(getApplicationContext());
// if success and newPatch, it is nice to delete the raw file, and restart at once
// only main process can load an upgrade patch!
if (result.isSuccess) {
deleteRawPatchFile(new File(result.rawPatchFilePath));
if (checkIfNeedKill(result)) {
android.os.Process.killProcess(android.os.Process.myPid());
} else {
TinkerLog.i(TAG, "I have already install the newly patch version!");
}
}
}
}
它先关闭了patch的service,然后删除了补丁文件,最后关闭自己,然后整个补丁合成的过程就结束了。
补丁加载过程
在Tinker源码解析系列(一)—Application代理机制中我们分析过,补丁加载的时机便是在app重新启动的时候,Application中调用了TinkerLoader的tryLoad
方法,那这里我们直接分析tryLoad
方法
public Intent tryLoad(TinkerApplication app, int tinkerFlag, boolean tinkerLoadVerifyFlag) {
Intent resultIntent = new Intent();
long begin = SystemClock.elapsedRealtime();
tryLoadPatchFilesInternal(app, tinkerFlag, tinkerLoadVerifyFlag, resultIntent);
long cost = SystemClock.elapsedRealtime() - begin;
ShareIntentUtil.setIntentPatchCostTime(resultIntent, cost);
return resultIntent;
}
可以看到,核心代码其实就是tryLoadPatchFilesInternal(app, tinkerFlag, tinkerLoadVerifyFlag, resultIntent)
,我们直接看看这个方法
private void tryLoadPatchFilesInternal(TinkerApplication app, int tinkerFlag, boolean tinkerLoadVerifyFlag, Intent resultIntent) {
if (!ShareTinkerInternals.isTinkerEnabled(tinkerFlag)) {
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_DISABLE);
return;
}
//tinker
File patchDirectoryFile = SharePatchFileUtil.getPatchDirectory(app);
if (patchDirectoryFile == null) {
Log.w(TAG, "tryLoadPatchFiles:getPatchDirectory == null");
//treat as not exist
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_DIRECTORY_NOT_EXIST);
return;
}
String patchDirectoryPath = patchDirectoryFile.getAbsolutePath();
//check patch directory whether exist
if (!patchDirectoryFile.exists()) {
Log.w(TAG, "tryLoadPatchFiles:patch dir not exist:" + patchDirectoryPath);
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_DIRECTORY_NOT_EXIST);
return;
}
//tinker/patch.info
File patchInfoFile = SharePatchFileUtil.getPatchInfoFile(patchDirectoryPath);
//check patch info file whether exist
if (!patchInfoFile.exists()) {
Log.w(TAG, "tryLoadPatchFiles:patch info not exist:" + patchInfoFile.getAbsolutePath());
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_INFO_NOT_EXIST);
return;
}
//old = 641e634c5b8f1649c75caf73794acbdf
//new = 2c150d8560334966952678930ba67fa8
File patchInfoLockFile = SharePatchFileUtil.getPatchInfoLockFile(patchDirectoryPath);
patchInfo = SharePatchInfo.readAndCheckPropertyWithLock(patchInfoFile, patchInfoLockFile);
if (patchInfo == null) {
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_INFO_CORRUPTED);
return;
}
String oldVersion = patchInfo.oldVersion;
String newVersion = patchInfo.newVersion;
if (oldVersion == null || newVersion == null) {
//it is nice to clean patch
Log.w(TAG, "tryLoadPatchFiles:onPatchInfoCorrupted");
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_INFO_CORRUPTED);
return;
}
resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_OLD_VERSION, oldVersion);
resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_NEW_VERSION, newVersion);
boolean mainProcess = ShareTinkerInternals.isInMainProcess(app);
boolean versionChanged = !(oldVersion.equals(newVersion));
String version = oldVersion;
if (versionChanged && mainProcess) {
version = newVersion;
}
if (ShareTinkerInternals.isNullOrNil(version)) {
Log.w(TAG, "tryLoadPatchFiles:version is blank, wait main process to restart");
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_INFO_BLANK);
return;
}
//patch-641e634c
String patchName = SharePatchFileUtil.getPatchVersionDirectory(version);
if (patchName == null) {
Log.w(TAG, "tryLoadPatchFiles:patchName is null");
//we may delete patch info file
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_VERSION_DIRECTORY_NOT_EXIST);
return;
}
//tinker/patch.info/patch-641e634c
String patchVersionDirectory = patchDirectoryPath + "/" + patchName;
File patchVersionDirectoryFile = new File(patchVersionDirectory);
if (!patchVersionDirectoryFile.exists()) {
Log.w(TAG, "tryLoadPatchFiles:onPatchVersionDirectoryNotFound");
//we may delete patch info file
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_VERSION_DIRECTORY_NOT_EXIST);
return;
}
//tinker/patch.info/patch-641e634c/patch-641e634c.apk
File patchVersionFile = new File(patchVersionDirectoryFile.getAbsolutePath(), SharePatchFileUtil.getPatchVersionFile(version));
if (!SharePatchFileUtil.isLegalFile(patchVersionFile)) {
Log.w(TAG, "tryLoadPatchFiles:onPatchVersionFileNotFound");
//we may delete patch info file
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_VERSION_FILE_NOT_EXIST);
return;
}
ShareSecurityCheck securityCheck = new ShareSecurityCheck(app);
int returnCode = ShareTinkerInternals.checkTinkerPackage(app, tinkerFlag, patchVersionFile, securityCheck);
if (returnCode != ShareConstants.ERROR_PACKAGE_CHECK_OK) {
Log.w(TAG, "tryLoadPatchFiles:checkTinkerPackage");
resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_PACKAGE_PATCH_CHECK, returnCode);
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_PACKAGE_CHECK_FAIL);
return;
}
resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_PACKAGE_CONFIG, securityCheck.getPackagePropertiesIfPresent());
final boolean isEnabledForDex = ShareTinkerInternals.isTinkerEnabledForDex(tinkerFlag);
if (isEnabledForDex) {
//tinker/patch.info/patch-641e634c/dex
boolean dexCheck = TinkerDexLoader.checkComplete(patchVersionDirectory, securityCheck, resultIntent);
if (!dexCheck) {
//file not found, do not load patch
Log.w(TAG, "tryLoadPatchFiles:dex check fail");
return;
}
}
final boolean isEnabledForNativeLib = ShareTinkerInternals.isTinkerEnabledForNativeLib(tinkerFlag);
if (isEnabledForNativeLib) {
//tinker/patch.info/patch-641e634c/lib
boolean libCheck = TinkerSoLoader.checkComplete(patchVersionDirectory, securityCheck, resultIntent);
if (!libCheck) {
//file not found, do not load patch
Log.w(TAG, "tryLoadPatchFiles:native lib check fail");
return;
}
}
//check resource
final boolean isEnabledForResource = ShareTinkerInternals.isTinkerEnabledForResource(tinkerFlag);
Log.w(TAG, "tryLoadPatchFiles:isEnabledForResource:" + isEnabledForResource);
if (isEnabledForResource) {
boolean resourceCheck = TinkerResourceLoader.checkComplete(app, patchVersionDirectory, securityCheck, resultIntent);
if (!resourceCheck) {
//file not found, do not load patch
Log.w(TAG, "tryLoadPatchFiles:resource check fail");
return;
}
}
//only work for art platform oat
boolean isSystemOTA = ShareTinkerInternals.isVmArt() && ShareTinkerInternals.isSystemOTA(patchInfo.fingerPrint);
resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_SYSTEM_OTA, isSystemOTA);
//we should first try rewrite patch info file, if there is a error, we can't load jar
if (isSystemOTA
|| (mainProcess && versionChanged)) {
patchInfo.oldVersion = version;
//update old version to new
if (!SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, patchInfo, patchInfoLockFile)) {
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_REWRITE_PATCH_INFO_FAIL);
Log.w(TAG, "tryLoadPatchFiles:onReWritePatchInfoCorrupted");
return;
}
}
if (!checkSafeModeCount(app)) {
resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, new TinkerRuntimeException("checkSafeModeCount fail"));
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_UNCAUGHT_EXCEPTION);
Log.w(TAG, "tryLoadPatchFiles:checkSafeModeCount fail");
return;
}
//now we can load patch jar
if (isEnabledForDex) {
boolean loadTinkerJars = TinkerDexLoader.loadTinkerJars(app, tinkerLoadVerifyFlag, patchVersionDirectory, resultIntent, isSystemOTA);
if (!loadTinkerJars) {
Log.w(TAG, "tryLoadPatchFiles:onPatchLoadDexesFail");
return;
}
}
//now we can load patch resource
if (isEnabledForResource) {
boolean loadTinkerResources = TinkerResourceLoader.loadTinkerResources(app, tinkerLoadVerifyFlag, patchVersionDirectory, resultIntent);
if (!loadTinkerResources) {
Log.w(TAG, "tryLoadPatchFiles:onPatchLoadResourcesFail");
return;
}
}
//all is ok!
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_OK);
Log.i(TAG, "tryLoadPatchFiles: load end, ok!");
return;
}
代码很长,还是同样,大部分是进行了一些安全性的校验,这里直接列出核心代码
-
TinkerDexLoader.loadTinkerJars(app, tinkerLoadVerifyFlag, patchVersionDirectory, resultIntent, isSystemOTA)
加载dex文件 -
TinkerResourceLoader.loadTinkerResources(app, tinkerLoadVerifyFlag, patchVersionDirectory, resultIntent)
加载资源文件
加载Dex
这里我们先看看TinkerDexLoader.loadTinkerJars
方法
public static boolean loadTinkerJars(Application application, boolean tinkerLoadVerifyFlag, String directory, Intent intentResult, boolean isSystemOTA) {
...
try {
SystemClassLoaderAdder.installDexes(application, classLoader, optimizeDir, legalFiles);
} catch (Throwable e) {
Log.e(TAG, "install dexes failed");
// e.printStackTrace();
intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, e);
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_LOAD_EXCEPTION);
return false;
}
return true;
}
方法很长,我们直接看一下它的核心方法SystemClassLoaderAdder.installDexes(application, classLoader, optimizeDir, legalFiles)
public static void installDexes(Application application, PathClassLoader loader, File dexOptDir, List<File> files)
throws Throwable {
if (!files.isEmpty()) {
ClassLoader classLoader = loader;
if (Build.VERSION.SDK_INT >= 24) {
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针对不同的系统版本都做了处理,这里我们为了节省篇幅,就挑V23来进行分析,
private static final class 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;
}
}
}
/**
* A wrapper around
* {@code private static final dalvik.system.DexPathList#makePathElements}.
*/
private static Object[] makePathElements(
Object dexPathList, ArrayList<File> files, File optimizedDirectory,
ArrayList<IOException> suppressedExceptions)
throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
Method makePathElements;
try {
makePathElements = ShareReflectUtil.findMethod(dexPathList, "makePathElements", List.class, File.class,
List.class);
} catch (NoSuchMethodException e) {
Log.e(TAG, "NoSuchMethodException: makePathElements(List,File,List) failure");
try {
makePathElements = ShareReflectUtil.findMethod(dexPathList, "makePathElements", ArrayList.class, File.class, ArrayList.class);
} catch (NoSuchMethodException e1) {
Log.e(TAG, "NoSuchMethodException: makeDexElements(ArrayList,File,ArrayList) failure");
try {
Log.e(TAG, "NoSuchMethodException: try use v19 instead");
return V19.makeDexElements(dexPathList, files, optimizedDirectory, suppressedExceptions);
} catch (NoSuchMethodException e2) {
Log.e(TAG, "NoSuchMethodException: makeDexElements(List,File,List) failure");
throw e2;
}
}
}
return (Object[]) makePathElements.invoke(dexPathList, files, optimizedDirectory, suppressedExceptions);
}
}
代码不少,但其实不复杂,就是通过反射获取到dexElements字段,然后通过将我们的合成补丁后的dex文件插入到这个数组的前端,从而使得已经打了补丁的dex能够优先被加载到,对这一块不是很清楚的可以看一下Android热修复原理探索与实践
加载资源文件
加载资源文件的过程的话首先是调用了TinkerResourcePatcher
的isResourceCanPatch
方法,判断是否能够进行加载补丁资源,若没有抛出异常则视为可以,源码如下
public static void isResourceCanPatch(Context context) throws Throwable {
// - Replace mResDir to point to the external resource file instead of the .apk. This is
// used as the asset path for new Resources objects.
// - Set Application#mLoadedApk to the found LoadedApk instance
// Find the ActivityThread instance for the current thread
Class<?> activityThread = Class.forName("android.app.ActivityThread");
currentActivityThread = ShareReflectUtil.getActivityThread(context, activityThread);
// API version 8 has PackageInfo, 10 has LoadedApk. 9, I don't know.
Class<?> loadedApkClass;
try {
loadedApkClass = Class.forName("android.app.LoadedApk");
} catch (ClassNotFoundException e) {
loadedApkClass = Class.forName("android.app.ActivityThread$PackageInfo");
}
resDir = loadedApkClass.getDeclaredField("mResDir");
resDir.setAccessible(true);
packagesFiled = activityThread.getDeclaredField("mPackages");
packagesFiled.setAccessible(true);
resourcePackagesFiled = activityThread.getDeclaredField("mResourcePackages");
resourcePackagesFiled.setAccessible(true);
// Create a new AssetManager instance and point it to the resources
AssetManager assets = context.getAssets();
// Baidu os
if (assets.getClass().getName().equals("android.content.res.BaiduAssetManager")) {
Class baiduAssetManager = Class.forName("android.content.res.BaiduAssetManager");
newAssetManager = (AssetManager) baiduAssetManager.getConstructor().newInstance();
} else {
newAssetManager = AssetManager.class.getConstructor().newInstance();
}
addAssetPathMethod = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
addAssetPathMethod.setAccessible(true);
// Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm
// in L, so we do it unconditionally.
ensureStringBlocksMethod = AssetManager.class.getDeclaredMethod("ensureStringBlocks");
ensureStringBlocksMethod.setAccessible(true);
// Iterate over all known Resources objects
if (SDK_INT >= KITKAT) {
//pre-N
// Find the singleton instance of ResourcesManager
Class<?> resourcesManagerClass = Class.forName("android.app.ResourcesManager");
Method mGetInstance = resourcesManagerClass.getDeclaredMethod("getInstance");
mGetInstance.setAccessible(true);
Object resourcesManager = mGetInstance.invoke(null);
try {
Field fMActiveResources = resourcesManagerClass.getDeclaredField("mActiveResources");
fMActiveResources.setAccessible(true);
ArrayMap<?, WeakReference<Resources>> activeResources19 =
(ArrayMap<?, WeakReference<Resources>>) fMActiveResources.get(resourcesManager);
references = activeResources19.values();
} catch (NoSuchFieldException ignore) {
// N moved the resources to mResourceReferences
Field mResourceReferences = resourcesManagerClass.getDeclaredField("mResourceReferences");
mResourceReferences.setAccessible(true);
// resourceImpls = (ArrayMap<?, WeakReference<?>>) mResourceReferences.get("mResourceImpls");
references = (Collection<WeakReference<Resources>>) mResourceReferences.get(resourcesManager);
}
} else {
Field fMActiveResources = activityThread.getDeclaredField("mActiveResources");
fMActiveResources.setAccessible(true);
HashMap<?, WeakReference<Resources>> activeResources7 =
(HashMap<?, WeakReference<Resources>>) fMActiveResources.get(currentActivityThread);
references = activeResources7.values();
}
// check resource
if (references == null) {
throw new IllegalStateException("resource references is null");
}
try {
assetsFiled = Resources.class.getDeclaredField("mAssets");
assetsFiled.setAccessible(true);
} catch (Throwable ignore) {
// N moved the mAssets inside an mResourcesImpl field
resourcesImplFiled = Resources.class.getDeclaredField("mResourcesImpl");
resourcesImplFiled.setAccessible(true);
}
}
代码特别多,实际上就是通过反射获取了AssetManager对象,其中还对百度os系统做了兼容(BaiduAssetManager),然后获取它的addAssetPath方法,然后通过TinkerResourceLoader
的loadTinkerResources
方法里的TinkerResourcePatcher.monkeyPatchExistingResources(context, resourceString)
,将补丁资源文件添加进去后,替换AssetManager对象,实现了补丁资源的加载。
具体代码就不再贴了,因为本文的篇幅实在够长了。
总结
能耐心看到这里的小伙伴们真的是不容易呀,给你们点个赞。
同个对整个Tinker源码的分析,确实不得不感叹Tinker对各种异常的处理以及各种版本兼容做得非常好,也充分印证了Tinker官方文档中的一句话
Tinker已运行在微信的数亿Android设备上,那么为什么你不使用Tinker呢?
所以不得不说,Tinker可能是迄今为止,最好的热修复框架。
因为我实力有限,所以可能分析得不是很深入,又或者说哪里有错误,大家都可以在评论中向我指出,通过阅读Tinker源码,也给大家分享一点阅读源码的经验,阅读源码还是推荐大家有一个目标,比如说要搞清楚Tinker的加载补丁的过程,那么就针对这一个点去看,而且不要过分纠结于细节,对整个大致的流程理清楚明白才是关键,过分纠结于某一行的代码的意义,只会让你最后阅读得痛苦不堪,难以继续。
好啦,谢谢大家~