Git项目地址:https://github.com/Tencent/tinker
本例解析tag为v1.9.14.19
Tinker is a hot-fix solution library for Android, it supports dex, library and resources update without reinstalling apk.
tinker作为Android一款热修复框架,实践应用在微信上,其稳定性,兼容性不言而喻;看下官方说明
上图来自tinker官方,我截了个图,可以看到tinker相比其他热修复框架优势还是非常明显的,作为Android开发有必要探究下起内部实现机制,今天就来扒一扒tinker皮
tinker基本接入步骤直接忽略,下面以tinker-sample-android为例进行逐步解析,既然Tinker是一款免安装针对Android的热补丁修复框架,热修复 必定绕过不二部分
先看下SampleApplication
这里的SampleApplication是由tinker-android-anno自动生成的,可以看到它且继承了TinkerApplication,且在构造器中传入了一个参数给父类,里面包含了代理类,自定义加载器类名等
我们直接看TinkerApplication.attachBaseContext好了
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
final long applicationStartElapsedTime = SystemClock.elapsedRealtime();
final long applicationStartMillisTime = System.currentTimeMillis();
Thread.setDefaultUncaughtExceptionHandler(new TinkerUncaughtHandler(this));
onBaseContextAttached(base, applicationStartElapsedTime, applicationStartMillisTime);
}
// TinkerApplication
protected void onBaseContextAttached(Context base,
long applicationStartElapsedTime,
long applicationStartMillisTime) {
try {
loadTinker();
mCurrentClassLoader = base.getClassLoader();
mInlineFence = createInlineFence(this, tinkerFlags, delegateClassName,
tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime,
tinkerResultIntent);
TinkerInlineFenceAction.callOnBaseContextAttached(mInlineFence, base);
//reset save mode
if (useSafeMode) {
ShareTinkerInternals.setSafeModeCount(this, 0);
}
} catch (TinkerRuntimeException e) {
throw e;
} catch (Throwable thr) {
throw new TinkerRuntimeException(thr.getMessage(), thr);
}
}
loadTinker是其tinker启动加载核心;内部其实就是寻找并解压补丁包,并动态加载
我们看到loadTinker代码实现非常简单,是调用loaderClassName即com.tencent.tinker.loader.TinkerLoader.tryLoad方法,当然这里的tinkerLoader对象也是反射搞出来的
tryLoad内部实现了啥?可以大胆猜测是对下发的补丁包进行解压,解压后有二类资源一类是dex文件,一类是资源文件;对于dex文件可以直接使用DexClassLoader实现动态加载,对于资源文件可以通过反射调用addAssetPath将资源告知给系统;接下来看下tinker内部实现是不是这样做的?
@Override
public Intent tryLoad(TinkerApplication app) {
ShareTinkerLog.d(TAG, "tryLoad test test");
Intent resultIntent = new Intent();
long begin = SystemClock.elapsedRealtime();
// 1. 核心代码
tryLoadPatchFilesInternal(app, resultIntent);
long cost = SystemClock.elapsedRealtime() - begin;
// 2. 记录补丁包耗时
ShareIntentUtil.setIntentPatchCostTime(resultIntent, cost);
return resultIntent;
}
tryLoadPatchFilesInternal内部实现有330行代码左右,但主流程非常清晰,我们以功能来划分逐部拆解即可,主要分为四个小块
private void tryLoadPatchFilesInternal(TinkerApplication app, Intent resultIntent) {
final int tinkerFlag = app.getTinkerFlags();
// 一些校验处理
if (!ShareTinkerInternals.isTinkerEnabled(tinkerFlag)) {
ShareTinkerLog.w(TAG, "tryLoadPatchFiles: tinker is disable, just return");
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_DISABLE);
return;
}
if (ShareTinkerInternals.isInPatchProcess(app)) {
ShareTinkerLog.w(TAG, "tryLoadPatchFiles: we don't load patch with :patch process itself, just return");
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_DISABLE);
return;
}
// 1. 获取补丁包位置
//tinker
File patchDirectoryFile = SharePatchFileUtil.getPatchDirectory(app);
if (patchDirectoryFile == null) {
ShareTinkerLog.w(TAG, "tryLoadPatchFiles:getPatchDirectory == null");
//treat as not exist
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_DIRECTORY_NOT_EXIST);
return;
}
String patchDirectoryPath = patchDirectoryFile.getAbsolutePath();
// 2. 补丁目录校验
//check patch directory whether exist
if (!patchDirectoryFile.exists()) {
ShareTinkerLog.w(TAG, "tryLoadPatchFiles:patch dir not exist:" + patchDirectoryPath);
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_DIRECTORY_NOT_EXIST);
return;
}
//tinker/patch.info 3. 读取补丁相关信息
File patchInfoFile = SharePatchFileUtil.getPatchInfoFile(patchDirectoryPath);
//check patch info file whether exist
if (!patchInfoFile.exists()) {
ShareTinkerLog.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);
// 4. 解析补丁信息
patchInfo = SharePatchInfo.readAndCheckPropertyWithLock(patchInfoFile, patchInfoLockFile);
if (patchInfo == null) {
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_INFO_CORRUPTED);
return;
}
}
解析补丁信息(readAndCheckPropertyWithLock)字段包括如下
old --> oldVer
new --> newVer
is_protected_app --> 加固app
is_remove_new_version
print --> finger print
dir --> oat dir
is_remove_interpret_oat_dir
private void tryLoadPatchFilesInternal(TinkerApplication app, Intent resultIntent) {
// ...
final boolean isProtectedApp = patchInfo.isProtectedApp;
resultIntent.putExtra(ShareIntentUtil.INTENT_IS_PROTECTED_APP, isProtectedApp);
String oldVersion = patchInfo.oldVersion;
String newVersion = patchInfo.newVersion;
String oatDex = patchInfo.oatDir;
if (oldVersion == null || newVersion == null || oatDex == null) {
//it is nice to clean patch
ShareTinkerLog.w(TAG, "tryLoadPatchFiles:onPatchInfoCorrupted");
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_INFO_CORRUPTED);
return;
}
boolean mainProcess = ShareTinkerInternals.isInMainProcess(app);
boolean isRemoveNewVersion = patchInfo.isRemoveNewVersion;
if (mainProcess) {
final String patchName = SharePatchFileUtil.getPatchVersionDirectory(newVersion);
// So far new version is not loaded in main process and other processes.
// We can remove new version directory safely.
if (isRemoveNewVersion) {
ShareTinkerLog.w(TAG, "found clean patch mark and we are in main process, delete patch file now.");
if (patchName != null) {
// oldVersion.equals(newVersion) means the new version has been loaded at least once
// after it was applied.
final boolean isNewVersionLoadedBefore = oldVersion.equals(newVersion);
if (isNewVersionLoadedBefore) {
// Set oldVersion and newVersion to empty string to clean patch
// if current patch has been loaded before.
oldVersion = "";
}
newVersion = oldVersion;
patchInfo.oldVersion = oldVersion;
patchInfo.newVersion = newVersion;
patchInfo.isRemoveNewVersion = false;
SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, patchInfo, patchInfoLockFile);
String patchVersionDirFullPath = patchDirectoryPath + "/" + patchName;
SharePatchFileUtil.deleteDir(patchVersionDirFullPath);
if (isNewVersionLoadedBefore) {
ShareTinkerInternals.killProcessExceptMain(app);
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_DIRECTORY_NOT_EXIST);
return;
}
}
}
if (patchInfo.isRemoveInterpretOATDir) {
// delete interpret odex
// for android o, directory change. Fortunately, we don't need to support android o interpret mode any more
ShareTinkerLog.i(TAG, "tryLoadPatchFiles: isRemoveInterpretOATDir is true, try to delete interpret optimize files");
patchInfo.isRemoveInterpretOATDir = false;
SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, patchInfo, patchInfoLockFile);
ShareTinkerInternals.killProcessExceptMain(app);
String patchVersionDirFullPath = patchDirectoryPath + "/" + patchName;
SharePatchFileUtil.deleteDir(patchVersionDirFullPath + "/" + ShareConstants.INTERPRET_DEX_OPTIMIZE_PATH);
}
}
resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_OLD_VERSION, oldVersion);
resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_NEW_VERSION, newVersion);
boolean versionChanged = !(oldVersion.equals(newVersion));
boolean oatModeChanged = oatDex.equals(ShareConstants.CHANING_DEX_OPTIMIZE_PATH);
oatDex = ShareTinkerInternals.getCurrentOatMode(app, oatDex);
resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_OAT_DIR, oatDex);
String version = oldVersion;
if (versionChanged && mainProcess) {
version = newVersion;
}
if (ShareTinkerInternals.isNullOrNil(version)) {
ShareTinkerLog.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) {
ShareTinkerLog.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()) {
ShareTinkerLog.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
final String patchVersionFileRelPath = SharePatchFileUtil.getPatchVersionFile(version);
File patchVersionFile = (patchVersionFileRelPath != null ? new File(patchVersionDirectoryFile.getAbsolutePath(), patchVersionFileRelPath) : null);
if (!SharePatchFileUtil.isLegalFile(patchVersionFile)) {
ShareTinkerLog.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) {
ShareTinkerLog.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());
// ...
}
private void tryLoadPatchFilesInternal(TinkerApplication app, Intent resultIntent) {
// ..
//tinker/patch.info/patch-641e634c/patch-641e634c.apk
final String patchVersionFileRelPath = SharePatchFileUtil.getPatchVersionFile(version);
File patchVersionFile = (patchVersionFileRelPath != null ? new File(patchVersionDirectoryFile.getAbsolutePath(), patchVersionFileRelPath) : null);
// 1. 校验apk文件是否可读
if (!SharePatchFileUtil.isLegalFile(patchVersionFile)) {
ShareTinkerLog.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);
// 2. 校验补丁包apk签名校验,以及TinkerID
int returnCode = ShareTinkerInternals.checkTinkerPackage(app, tinkerFlag, patchVersionFile, securityCheck);
if (returnCode != ShareConstants.ERROR_PACKAGE_CHECK_OK) {
ShareTinkerLog.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());
}
上述一大坨代码,都是一些清理,和补丁包apk的校验操作
assets/so_meta.txt格式
$name,$path,$md5,$rawCrc,$pathMd5
对应ShareBsDiffPatchInfo类
assets/res_meta.txt格式 ==> ShareResPatchInfo
$arscBasseCrc,$resArscMd5 //firstLine
final boolean isEnabledForDex = ShareTinkerInternals.isTinkerEnabledForDex(tinkerFlag);
// 方舟编译器
final boolean isArkHotRuning = ShareTinkerInternals.isArkHotRuning();
if (!isArkHotRuning && isEnabledForDex) {
// 3. 解析dex_meta.txt以校验dex是否有丢失(所有dex是否都在assets/dex_meta.txt中)
//tinker/patch.info/patch-641e634c/dex
boolean dexCheck = TinkerDexLoader.checkComplete(patchVersionDirectory, securityCheck, oatDex, resultIntent);
if (!dexCheck) {
//file not found, do not load patch
ShareTinkerLog.w(TAG, "tryLoadPatchFiles:dex check fail");
return;
}
}
先说下dex_meta.txt配置文件格式
$name,$path,$destMd5InDvm,$dexMd5InArt,$dexDiffMd5,$oldDexCrc,$newDexCrc,$dexMode
接下来逐步分析如何校验dex文件
/**
* all the dex files in meta file exist?
* fast check, only check whether exist
*
* @return boolean
*/
public static boolean checkComplete(String directory, ShareSecurityCheck securityCheck, String oatDir, Intent intentResult) {
// 1. 获取dex_meta.txt文件内容
String meta = securityCheck.getMetaContentMap().get(DEX_MEAT_FILE);
//not found dex
if (meta == null) {
return true;
}
LOAD_DEX_LIST.clear();
classNDexInfo.clear();
ArrayList<ShareDexDiffPatchInfo> allDexInfo = new ArrayList<>();
// 2. 解析dex_meta.txt内容
ShareDexDiffPatchInfo.parseDexDiffPatchInfo(meta, allDexInfo);
if (allDexInfo.isEmpty()) {
return true;
}
HashMap<String, String> dexes = new HashMap<>();
ShareDexDiffPatchInfo testInfo = null;
// 3. 对解析出的dex相关信息列表进行遍历
for (ShareDexDiffPatchInfo info : allDexInfo) {
// dalvik虚拟机不支持多dex加载,直接忽略
//for dalvik, ignore art support dex
if (isJustArtSupportDex(info)) {
continue;
}
// rawName及md5长度校验
if (!ShareDexDiffPatchInfo.checkDexDiffPatchInfo(info)) {
intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_PACKAGE_PATCH_CHECK, ShareConstants.ERROR_PACKAGE_CHECK_DEX_META_CORRUPTED);
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_PACKAGE_CHECK_FAIL);
return false;
}
// 过滤test.dex
if (isVmArt && info.rawName.startsWith(ShareConstants.TEST_DEX_NAME)) {
testInfo = info;
} else if (isVmArt && ShareConstants.CLASS_N_PATTERN.matcher(info.realName).matches()) {
classNDexInfo.add(info);
} else {
// 记录dex
dexes.put(info.realName, getInfoMd5(info));
LOAD_DEX_LIST.add(info);
}
}
if (isVmArt
&& (testInfo != null || !classNDexInfo.isEmpty())) {
if (testInfo != null) {
classNDexInfo.add(ShareTinkerInternals.changeTestDexToClassN(testInfo, classNDexInfo.size() + 1));
}
dexes.put(ShareConstants.CLASS_N_APK_NAME, "");
}
//tinker/patch.info/patch-641e634c/dex
String dexDirectory = directory + "/" + DEX_PATH + "/";
File dexDir = new File(dexDirectory);
// dexDir相关校验
if (!dexDir.exists() || !dexDir.isDirectory()) {
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_DIRECTORY_NOT_EXIST);
return false;
}
String optimizeDexDirectory = directory + "/" + oatDir + "/";
File optimizeDexDirectoryFile = new File(optimizeDexDirectory);
//fast check whether there is any dex files missing
for (String name : dexes.keySet()) {
File dexFile = new File(dexDirectory + name);
// 对每个dex文件做合法(存在&可读)判断
if (!SharePatchFileUtil.isLegalFile(dexFile)) {
intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_MISSING_DEX_PATH, dexFile.getAbsolutePath());
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_FILE_NOT_EXIST);
return false;
}
//check dex opt whether complete also
File dexOptFile = new File(SharePatchFileUtil.optimizedPathFor(dexFile, optimizeDexDirectoryFile));
// 对odex文件进行校验处理
if (!SharePatchFileUtil.isLegalFile(dexOptFile)) {
if (SharePatchFileUtil.shouldAcceptEvenIfIllegal(dexOptFile)) {
continue;
}
intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_MISSING_DEX_PATH, dexOptFile.getAbsolutePath());
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_OPT_FILE_NOT_EXIST);
return false;
}
// // find test dex
// if (dexOptFile.getName().startsWith(ShareConstants.TEST_DEX_NAME)) {
// testOptDexFile = dexOptFile;
// }
}
//if is ok, add to result intent
intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_DEXES_PATH, dexes);
return true;
}
这部操作总结如下:
// 4. 校验是否运行在华为ark环境下且是否可用
final boolean isEnabledForArkHot = ShareTinkerInternals.isTinkerEnabledForArkHot(tinkerFlag);
if (isArkHotRuning && isEnabledForArkHot) {
boolean arkHotCheck = TinkerArkHotLoader.checkComplete(patchVersionDirectory, securityCheck, resultIntent);
if (!arkHotCheck) {
// file not found, do not load patch
ShareTinkerLog.w(TAG, "tryLoadPatchFiles:dex check fail");
return;
}
}
// 5. 校验so文件
final boolean isEnabledForNativeLib = ShareTinkerInternals.isTinkerEnabledForNativeLib(tinkerFlag);
if (isEnabledForNativeLib) {
// 解析so_meta.tx文件以校验so文件是否存在丢失
//tinker/patch.info/patch-641e634c/lib
boolean libCheck = TinkerSoLoader.checkComplete(patchVersionDirectory, securityCheck, resultIntent);
if (!libCheck) {
//file not found, do not load patch
ShareTinkerLog.w(TAG, "tryLoadPatchFiles:native lib check fail");
return;
}
}
so库校验其实就是解析so_meta.tx文件中信息以校验meta中记载的文件是否存在
so_meta.tx每行格式如下(对应ShareBsDiffPatchInfo类)
$name,$path,$md5,$rawCrc,$pathMd5
// 6. 资源校验
//check resource
final boolean isEnabledForResource = ShareTinkerInternals.isTinkerEnabledForResource(tinkerFlag);
ShareTinkerLog.w(TAG, "tryLoadPatchFiles:isEnabledForResource:" + isEnabledForResource);
if (isEnabledForResource) {
// 校验资源包完整性;dir/res/resources.apk是否存在且可读;资源是否可以patch()
boolean resourceCheck = TinkerResourceLoader.checkComplete(app, patchVersionDirectory, securityCheck, resultIntent);
if (!resourceCheck) {
//file not found, do not load patch
ShareTinkerLog.w(TAG, "tryLoadPatchFiles:resource check fail");
return;
}
}
总结:资源校验其实分为二个步骤
public static boolean checkComplete(Context context, String directory, ShareSecurityCheck securityCheck, Intent intentResult) {
String meta = securityCheck.getMetaContentMap().get(RESOURCE_META_FILE);
//not found resource
if (meta == null) {
return true;
}
//only parse first line for faster
ShareResPatchInfo.parseResPatchInfoFirstLine(meta, resPatchInfo);
if (resPatchInfo.resArscMd5 == null) {
return true;
}
// 完整性校验
if (!ShareResPatchInfo.checkResPatchInfo(resPatchInfo)) {
intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_PACKAGE_PATCH_CHECK, ShareConstants.ERROR_PACKAGE_CHECK_RESOURCE_META_CORRUPTED);
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_PACKAGE_CHECK_FAIL);
return false;
}
String resourcePath = directory + "/" + RESOURCE_PATH + "/";
File resourceDir = new File(resourcePath);
if (!resourceDir.exists() || !resourceDir.isDirectory()) {
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_RESOURCE_DIRECTORY_NOT_EXIST);
return false;
}
File resourceFile = new File(resourcePath + RESOURCE_FILE);
// resources.apk是否存在可读
if (!SharePatchFileUtil.isLegalFile(resourceFile)) {
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_RESOURCE_FILE_NOT_EXIST);
return false;
}
try {
// 2. 校验资源是否可以进行修复
TinkerResourcePatcher.isResourceCanPatch(context);
} catch (Throwable e) {
ShareTinkerLog.e(TAG, "resource hook check failed.", e);
intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, e);
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_RESOURCE_LOAD_EXCEPTION);
return false;
}
return true;
}
这步主要做了如下:
assets/res_meta.txt格式如下 ==> ShareResPatchInfo
$arscBasseCrc,$resArscMd5 //firstLine
TinkerResourcePatcher.isResourceCanPatch(context)
4.4-7.0)获取取android.app.ResourcesManage.getInstance()中的mActiveResources属性,
7.0及以后开始获取mActiveResources属性
4.4以前获取android.app.ActivityThread中的mActiveResources
之所以要说这个是,在android不同版本情况下,app安装时针对apk会启用不同编译模式处理,这就导致单纯地通过将补丁包中的dex动态插入到dexElements到前面不再那么好使,所以要分为治之
ART vs Dalvik
Android Runtime (ART) 是 Android 上的应用和部分系统服务使用的托管式运行时。ART 及其前身 Dalvik 最初是专为 Android 项目打造的。作为运行时的 ART 可执行 Dalvik 可执行文件并遵循 Dex 字节码规范。
ART 和 Dalvik 是运行 Dex 字节码的兼容运行时,因此针对 Dalvik 开发的应用也能在 ART 环境中运作。不过,Dalvik 采用的一些技术并不适用于 ART
JIT:just-in-time 即时编译,边运行边编译(Android2.2版本被引入,4.4版本之后被AOT替代),JIT只对热点函数,热点trace进行编译,非热点函授还是走解释器;JIT编译生成的机器码存储在内存中,app下起启动时需要重新编译热点代码
参考:https://source.android.com/devices/tech/dalvik/jit-compiler?hl=zh-cn
JIT架构
JIT 编译
ART虚拟机引入了AOT预编译模式,旨在提高应用性能;在APK安装时使用设备自带的dex2oat工具编译应用
AOT:ahead of time 提前编译,即安装app时编译成机器码存储到硬盘。这样app运行时直接从本地取到机器码然后执行,提高代码执行效率
.dex --> dex2oat —> .oat文件
Android不同版本启用模式差异如下
~ 至4.4 JIT编译模式
5.x、6.x默认 AOT模式
7.0(Android-N)混合编译模式,开始结合使用AOT、JIT编译和配置文件引导型编译
private void tryLoadPatchFilesInternal(TinkerApplication app, Intent resultIntent) {
// ...
// 判断是否系统进行OTA升级 【5.0,8.0)
//only work for art platform oat,because of interpret, refuse 4.4 art oat
//android o use quicken default, we don't need to use interpret mode
boolean isSystemOTA = ShareTinkerInternals.isVmArt()
&& ShareTinkerInternals.isSystemOTA(patchInfo.fingerPrint)
&& Build.VERSION.SDK_INT >= 21 && !ShareTinkerInternals.isAfterAndroidO();
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 (mainProcess) {
if (versionChanged) {
patchInfo.oldVersion = version;
}
if (oatModeChanged) {
patchInfo.oatDir = oatDex;
patchInfo.isRemoveInterpretOATDir = true;
}
}
// 判断是否是安全模式(加载补丁是否失败超过了三次,超过三次后直接删除对应补丁回退)
if (!checkSafeModeCount(app)) {
if (mainProcess) {
// 主进程杀死其他同应用的进程,同时删除补丁包
// Mark current patch as deleted so that other process will not load patch after reboot.
patchInfo.oldVersion = "";
patchInfo.newVersion = "";
patchInfo.isRemoveNewVersion = false;
SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, patchInfo, patchInfoLockFile);
ShareTinkerInternals.killProcessExceptMain(app);
// Actually delete patch files.
String patchVersionDirFullPath = patchDirectoryPath + "/" + patchName;
SharePatchFileUtil.deleteDir(patchVersionDirFullPath);
resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, new TinkerRuntimeException("checkSafeModeCount fail"));
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_UNCAUGHT_EXCEPTION);
ShareTinkerLog.w(TAG, "tryLoadPatchFiles:checkSafeModeCount fail, patch was deleted.");
return;
} else {
// isRemoveNewVersion设置为true,以便下次启动时清除patch
ShareTinkerLog.w(TAG, "tryLoadPatchFiles:checkSafeModeCount fail, but we are not in main process, mark the patch to be deleted and continue load patch.");
ShareTinkerInternals.cleanPatch(app);
}
}
// ...
}
//now we can load patch jar
if (!isArkHotRuning && isEnabledForDex) {
// load tinker jars
boolean loadTinkerJars = TinkerDexLoader.loadTinkerJars(app, patchVersionDirectory, oatDex, resultIntent, isSystemOTA, isProtectedApp);
if (isSystemOTA) {
// update fingerprint after load success
...
// 针对OTA做的一些处理
}
if (!loadTinkerJars) {
ShareTinkerLog.w(TAG, "tryLoadPatchFiles:onPatchLoadDexesFail");
return;
}
}
if (isArkHotRuning && isEnabledForArkHot) {
boolean loadArkHotFixJars = TinkerArkHotLoader.loadTinkerArkHot(app, patchVersionDirectory, resultIntent);
if (!loadArkHotFixJars) {
ShareTinkerLog.w(TAG, "tryLoadPatchFiles:onPatchLoadArkApkFail");
return;
}
}
这部分主要做了如下操作
我们先看看loadTinkerJars吧
/**
* Load tinker JARs and add them to
* the Application ClassLoader.
*
* @param application The application.
*/
public static boolean loadTinkerJars(final TinkerApplication application, String directory, String oatDir, Intent intentResult, boolean isSystemOTA, boolean isProtectedApp) {
// 1. check下是否存有dex(校验dex步骤回解析出相关的dex文件)
if (LOAD_DEX_LIST.isEmpty() && classNDexInfo.isEmpty()) {
ShareTinkerLog.w(TAG, "there is no dex to load");
return true;
}
// 2. classLoader check
ClassLoader classLoader = TinkerDexLoader.class.getClassLoader();
if (classLoader != null) {
ShareTinkerLog.i(TAG, "classloader: " + classLoader.toString());
} else {
ShareTinkerLog.e(TAG, "classloader is null");
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_CLASSLOADER_NULL);
return false;
}
String dexPath = directory + "/" + DEX_PATH + "/";
ArrayList<File> legalFiles = new ArrayList<>();
for (ShareDexDiffPatchInfo info : LOAD_DEX_LIST) {
//for dalvik, ignore art support dex
if (isJustArtSupportDex(info)) {
continue;
}
String path = dexPath + info.realName;
File file = new File(path);
if (application.isTinkerLoadVerifyFlag()) {
long start = System.currentTimeMillis();
String checkMd5 = getInfoMd5(info);
// 3. dex完整性校验
if (!SharePatchFileUtil.verifyDexFileMd5(file, checkMd5)) {
//it is good to delete the mismatch file
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_MD5_MISMATCH);
intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_MISMATCH_DEX_PATH,
file.getAbsolutePath());
return false;
}
ShareTinkerLog.i(TAG, "verify dex file:" + file.getPath() + " md5, use time: " + (System.currentTimeMillis() - start));
}
// 4. 记录合法dex文件
legalFiles.add(file);
}
// verify merge classN.apk
if (isVmArt && !classNDexInfo.isEmpty()) {
File classNFile = new File(dexPath + ShareConstants.CLASS_N_APK_NAME);
long start = System.currentTimeMillis();
if (application.isTinkerLoadVerifyFlag()) {
for (ShareDexDiffPatchInfo info : classNDexInfo) {
// 5. 从tinker_classN.apk中提取dex进行完整性校验
if (!SharePatchFileUtil.verifyDexFileMd5(classNFile, info.rawName, info.destMd5InArt)) {
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_MD5_MISMATCH);
intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_MISMATCH_DEX_PATH,
classNFile.getAbsolutePath());
return false;
}
}
}
ShareTinkerLog.i(TAG, "verify dex file:" + classNFile.getPath() + " md5, use time: " + (System.currentTimeMillis() - start));
legalFiles.add(classNFile);
}
// 6. oat逻辑处理
File optimizeDir = new File(directory + "/" + oatDir);
if (isSystemOTA) {
final boolean[] parallelOTAResult = {true};
final Throwable[] parallelOTAThrowable = new Throwable[1];
String targetISA;
try {
targetISA = ShareTinkerInternals.getCurrentInstructionSet();
} catch (Throwable throwable) {
ShareTinkerLog.i(TAG, "getCurrentInstructionSet fail:" + throwable);
// try {
// targetISA = ShareOatUtil.getOatFileInstructionSet(testOptDexFile);
// } catch (Throwable throwable) {
// don't ota on the front
deleteOutOfDateOATFile(directory);
intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_INTERPRET_EXCEPTION, throwable);
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_GET_OTA_INSTRUCTION_SET_EXCEPTION);
return false;
// }
}
// 清理oat文件
deleteOutOfDateOATFile(directory);
ShareTinkerLog.w(TAG, "systemOTA, try parallel oat dexes, targetISA:" + targetISA);
// change dir
optimizeDir = new File(directory + "/" + INTERPRET_DEX_OPTIMIZE_PATH);
// 解释模式 dex2oat优化
TinkerDexOptimizer.optimizeAll(
application, legalFiles, optimizeDir, true,
application.isUseDelegateLastClassLoader(), targetISA,
new TinkerDexOptimizer.ResultCallback() {
long start;
@Override
public void onStart(File dexFile, File optimizedDir) {
start = System.currentTimeMillis();
ShareTinkerLog.i(TAG, "start to optimize dex:" + dexFile.getPath());
}
@Override
public void onSuccess(File dexFile, File optimizedDir, File optimizedFile) {
// Do nothing.
ShareTinkerLog.i(TAG, "success to optimize dex " + dexFile.getPath() + ", use time " + (System.currentTimeMillis() - start));
}
@Override
public void onFailed(File dexFile, File optimizedDir, Throwable thr) {
parallelOTAResult[0] = false;
parallelOTAThrowable[0] = thr;
ShareTinkerLog.i(TAG, "fail to optimize dex " + dexFile.getPath() + ", use time " + (System.currentTimeMillis() - start));
}
}
);
if (!parallelOTAResult[0]) {
ShareTinkerLog.e(TAG, "parallel oat dexes failed");
intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_INTERPRET_EXCEPTION, parallelOTAThrowable[0]);
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_OTA_INTERPRET_ONLY_EXCEPTION);
return false;
}
}
try {
final boolean useDLC = application.isUseDelegateLastClassLoader();
// 安装dex
SystemClassLoaderAdder.installDexes(application, classLoader, optimizeDir, legalFiles, isProtectedApp, useDLC);
} catch (Throwable e) {
ShareTinkerLog.e(TAG, "install dexes failed");
intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, e);
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_LOAD_EXCEPTION);
return false;
}
return true;
}
public static void installDexes(Application application, ClassLoader loader, File dexOptDir, List<File> files,
boolean isProtectedApp, boolean useDLC) throws Throwable {
ShareTinkerLog.i(TAG, "installDexes dexOptDir: " + dexOptDir.getAbsolutePath() + ", dex size:" + files.size());
if (!files.isEmpty()) {
files = createSortedAdditionalPathEntries(files);
ClassLoader classLoader = loader;
//[7.0之后,使用NewClassLoader规避混合编译带来的问题
if (Build.VERSION.SDK_INT >= 24 && !isProtectedApp) {
classLoader = NewClassLoaderInjector.inject(application, loader, dexOptDir, useDLC, files);
} else {
// dex插到classLoader.pathList.dexElements前面
injectDexesInternal(classLoader, files, dexOptDir);
}
//install done
sPatchDexCount = files.size();
ShareTinkerLog.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);
}
}
}
7.0之前加载dex比较简单,这里以6.0为例
从上图可以看到读取loader中的pathList(DexPathList)中dexElements,热修复的dex会插入到dexElements数组前面,这样就达到热修复目的,为什么?这涉及到类加载机制了
/**
* Base class for common functionality between various dex-based
* {@link ClassLoader} implementations.
*/
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
/**
* Constructs an instance.
*
* @param dexPath the list of jar/apk files containing classes and
* resources, delimited by {@code File.pathSeparator}, which
* defaults to {@code ":"} on Android
* @param optimizedDirectory directory where optimized dex files
* should be written; may be {@code null}
* @param libraryPath the list of directories containing native
* libraries, delimited by {@code File.pathSeparator}; may be
* {@code null}
* @param parent the parent class loader
*/
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
/**
* @hide
*/
public String getLdLibraryPath() {
StringBuilder result = new StringBuilder();
for (File directory : pathList.getNativeLibraryDirectories()) {
if (result.length() > 0) {
result.append(':');
}
result.append(directory);
}
return result.toString();
}
}
为什么要插到pathList中?这里需要了解下java类加载机制了,java采用双亲代理机制模型,说白了就是类加载都是先交给父classLoader去加载,如果父类搞不定则由自己来解决
看到BaseDexClassLoader.findClass方法就明白了,而加载类是依次从pathList中寻找的;
顺便说下getLdLibraryPath方法应该获取so库的路径,猜想下tinker对so的处理也会涉及到pathList中的nativeLibraryDirectories
先了解下ART运行方式
ART 的运作方式
ART 使用预先 (AOT) 编译,并且从 Android 7.0(代号 Nougat,简称 N)开始结合使用 AOT、即时 (JIT) 编译和配置文件引导型编译。所有这些编译模式的组合均可配置,我们将在本部分中对此进行介绍。例如,Pixel 设备配置了以下编译流程:
- 最初安装应用时不进行任何 AOT 编译。应用前几次运行时,系统会对其进行解译,并对经常执行的方法进行 JIT 编译。
- 当设备闲置和充电时,编译守护程序会运行,以便根据在应用前几次运行期间生成的配置文件对常用代码进行 AOT 编译。
- 下一次重新启动应用时将会使用配置文件引导型代码,并避免在运行时对已经过编译的方法进行 JIT 编译。在应用后续运行期间经过 JIT 编译的方法将会添加到配置文件中,然后编译守护程序将会对这些方法进行 AOT 编译。
ART 包括一个编译器(
dex2oat
工具)和一个为启动 Zygote 而加载的运行时 (libart.so
)。dex2oat
工具接受一个 APK 文件,并生成一个或多个编译工件文件,然后运行时将会加载这些文件。文件的个数、扩展名和名称因版本而异,但在 Android O 版本中,将会生成以下文件:
.vdex
:其中包含 APK 的未压缩 DEX 代码,以及一些旨在加快验证速度的元数据。.odex
:其中包含 APK 中已经过 AOT 编译的方法代码。.art (optional)
:其中包含 APK 中列出的某些字符串和类的 ART 内部表示,用于加快应用启动速度。
微信对于N上混合编译的解决方案是使用新的ClassLoader来加载后续的所有类,这样尽管牺牲了App Image带来的优化性能
无论是使用插入pathlist还是parent classloader的方式,若补丁修改的class已经存在与app image,它们都是无法通过热补丁更新的。它们在启动app时已经加入到PathClassloader的ClassTable中,系统在查找类时会直接使用base.apk中的class**。**
口说无凭,我们看下7.0中classLoader类loadClass和之前版本到底有什么差异性?
protected final Class<?> findLoadedClass(String name) {
ClassLoader loader;
if (this == BootClassLoader.getInstance())
loader = null;
else
loader = this;
return VMClassLoader.findLoadedClass(loader, name);
}
7.0开始VMClassLoder.findLoaderClass 会先从ClassLinker.LookupClass方法中取
继续跟踪下去原来ClassLinker.LookupClass方法是先从classtable中寻找类,如果有直接返回
到这时,我们明白了为什么7.0开始通过动态加载dex这种方式会失效了,就是classLoader本身自带的缓存导致的;为解决这问题,Tinker的解决方案是自定类加载器以规避该问题,但首次会有一定的性能损耗
接下来看Tinker在7.0及后面版本如何自定义DexClassLoader及实现注入的
// NewClassLoaderInjector.java
public static ClassLoader inject(Application app, ClassLoader oldClassLoader, File dexOptDir,
boolean useDLC, List<File> patchedDexes) throws Throwable {
final String[] patchedDexPaths = new String[patchedDexes.size()];
for (int i = 0; i < patchedDexPaths.length; ++i) {
patchedDexPaths[i] = patchedDexes.get(i).getAbsolutePath();
}
// 1. 创建新的classLoader
final ClassLoader newClassLoader = createNewClassLoader(oldClassLoader,
dexOptDir, useDLC, true, patchedDexPaths);
// 2. 注入自定义的classLoader
doInject(app, newClassLoader);
return newClassLoader;
}
创建新的classLoader并迁移数据
将需要加载的patchDex路径,及原so库路径传递给新classLoader
注入自定义classLoader
通过自定义代理ClassLoader实现了运行时先从代理ClassLoader加载类,后从原始ClassLoader加载,以解决混合编译模式下热修复失效问题
代码
//now we can load patch resource
if (isEnabledForResource) {
boolean loadTinkerResources = TinkerResourceLoader.loadTinkerResources(app, patchVersionDirectory, resultIntent);
if (!loadTinkerResources) {
ShareTinkerLog.w(TAG, "tryLoadPatchFiles:onPatchLoadResourcesFail");
return;
}
}
资源加载相对来说比较简单
资源映射表文件resources.arsc md5校验
TinkerResourcePatcher.monkeyPatchExistingResources
贴下代码
/**
* @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};
}
// 1. ActivityThread中mPackages、mResourcepackages中**LoaderApk**中的**resDir**为新的资源包路径
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");
}
// 2. 补充分享库到newAssetmanager
// Add SharedLibraries to AssetManager for resolve system resources not found issue
// This influence SharedLibrary Package ID
if (shouldAddSharedLibraryAssets(appInfo)) {
for (String sharedLibrary : appInfo.sharedLibraryFiles) {
if (!sharedLibrary.endsWith(".apk")) {
continue;
}
if (((Integer) addAssetPathAsSharedLibraryMethod.invoke(newAssetManager, sharedLibrary)) == 0) {
throw new IllegalStateException("AssetManager add SharedLibrary Fail");
}
Log.i(TAG, "addAssetPathAsSharedLibrary " + sharedLibrary);
}
}
// 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);
}
// 3. 对ResourcesManager.mActiveResources对象遍历。
// 绑定到新的newAssetManger
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 {
// 4. 将Resources.mAssets对象替换成新建的newAssetManger
//pre-N
assetsFiled.set(resources, newAssetManager);
} catch (Throwable ignore) {
// N开始替换Resources.mResourcesImpl.mAsset属性
// 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());
}
// WebView适配问题
// 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) {
// Ignored.
}
}
// 5. 验证资源是否正常加载成功
// 根据读取assets目录下资源文件only_use_to_test_tinker_resource.txt
if (!checkResUpdate(context)) {
throw new TinkerRuntimeException(ShareConstants.CHECK_RES_INSTALL_FAIL);
}
}
// Init component hotplug support.
if ((isEnabledForDex || isEnabledForArkHot) && isEnabledForResource) {
ComponentHotplug.install(app, securityCheck);
}
目前Tinker只支持Android四大组件中的Activity的热修复,Java本身是就具备动态化能力,在Android要启动一个Activity,AMS其实会对这个Activity做校验,AMS是单独的一个进程,我们无法做到hook AMS校验能力;通常的解决方式是偷梁换柱
而tinker其实也是这么做的,只是实现方式比网上很多实现的更优雅些
public synchronized static void install(TinkerApplication app, ShareSecurityCheck checker) throws UnsupportedEnvironmentException {
if (!sInstalled) {
try {
// 1. 解析补丁包中组件activity信息
if (IncrementComponentManager.init(app, checker)) {
// 2. hook startActivity相关方法
sAMSInterceptor = new ServiceBinderInterceptor(app, EnvConsts.ACTIVITY_MANAGER_SRVNAME, new AMSInterceptHandler(app));
sPMSInterceptor = new ServiceBinderInterceptor(app, EnvConsts.PACKAGE_MANAGER_SRVNAME, new PMSInterceptHandler());
sAMSInterceptor.install();
sPMSInterceptor.install();
// 3. hook ams通知app启动activity方法
if (Build.VERSION.SDK_INT < 27) {
final Handler mH = fetchMHInstance(app);
sMHMessageInterceptor = new HandlerMessageInterceptor(mH, new MHMessageHandler(app));
sMHMessageInterceptor.install();
} else {
sTinkerHackInstrumentation = TinkerHackInstrumentation.create(app);
sTinkerHackInstrumentation.install();
}
sInstalled = true;
ShareTinkerLog.i(TAG, "installed successfully.");
}
} catch (Throwable thr) {
uninstall();
throw new UnsupportedEnvironmentException(thr);
}
}
}
IncrementComponentManager.init方法就是用来干这事的,它主要从assets/inc_component_meta.txt文件中解析组件Activity信息
从解析组件代码来看,目前Tinker只支持组件activity,暂不支持service,receiver,provider组件热修复
activity启动简单回顾(以android9.0-api28为例)
context.startActivity -> startActivityForResult -> mInstrumentation.execStartActivity -> ActivityManager.getService().startActivity
红色圈圈里面其实涉及到跨进程调用,看下ActivityManger.getService()内部实现,如果熟悉binder的同学应该知道返回的是AMS在客户端的一个代理引用,也就是下图中的am
要把跳转的RealActivity替换成StubActivity,则需要hook am,因为Activity的创建,启动等都是AMS来管理,我们hook它在app中的代理对象即可
接下来我们看下tinker是如何hook am的,先看Interceptor类install方法
public synchronized void install() throws InterceptFailedException {
try {
final T_TARGET target = fetchTarget();
mTarget = target;
final T_TARGET decorated = decorate(target);
if (decorated != target) {
inject(decorated);
} else {
ShareTinkerLog.w(TAG, "target: " + target + " was already hooked.");
}
mInstalled = true;
} catch (Throwable thr) {
mTarget = null;
throw new InterceptFailedException(thr);
}
}
基本流程是先拿到需要hook的目标对象,紧接着对其进行包装,最后在执行注入操作;整个流程很简单,接下来看其实现类ServiceBinderInterceptor
静态代码块,拿到ServiceManger的sCache属性及getService方法,紧接找获取需要hook的对象
@Override
protected IBinder fetchTarget() throws Throwable {
// mServiceName为activity获取的就是binder对象
// 如果是package就是pm(PMS在客户端的代理对象)
return (IBinder) sGetServiceMethod.invoke(null, mServiceName);
}
包装远程binder引用
@Override
protected IBinder decorate(IBinder target) throws Throwable {
if (target == null) {
throw new IllegalStateException("target is null.");
}
// 如果对象是实现了ITinkerHotplugProxy接口(只是一个标志)说明已经被包装过了,直接返回即可
if (ITinkerHotplugProxy.class.isAssignableFrom(target.getClass())) {
// Already intercepted, just return the target.
return target;
} else {
// 通过java动态代理方式创建一个代理对象,并返回
return createProxy(getAllInterfacesThroughDeriveChain(target.getClass()),
new FakeClientBinderHandler(target, mBinderInvocationHandler));
}
}
这样所有AMS远程代理对象的方法调用都被转移到FakeClientBinderHandler类中invoke方法了,
@Override
public Object invoke(Object fakeClientBinder, Method method, Object[] args) throws Throwable {
if ("queryLocalInterface".equals(method.getName())) {
final String itfName = mOriginalClientBinder.getInterfaceDescriptor();
String stubClassName = null;
if (itfName.equals("android.app.IActivityManager")) {
stubClassName = "android.app.ActivityManagerNative";
} else {
stubClassName = itfName + "$Stub";
}
// 调用android.app.ActivityManagerNative.asInterface静态方法(返回了一个本地代理对象)
// 内部实现也就是IActivityManager.Stub.asInterface(obj);
final Class<?> stubClazz = Class.forName(stubClassName);
final Method asInterfaceMethod
= ShareReflectUtil.findMethod(stubClazz, "asInterface", IBinder.class);
// 本地代理对象
final IInterface originalInterface
= (IInterface) asInterfaceMethod.invoke(null, mOriginalClientBinder);
final InvocationHandler fakeInterfaceHandler
= new FakeInterfaceHandler(originalInterface, (IBinder) fakeClientBinder, mBinderInvocationHandler);
// 对本地代理对象,再次使用动态代理方式进行包装
return createProxy(getAllInterfacesThroughDeriveChain(originalInterface.getClass()), fakeInterfaceHandler);
} else {
return method.invoke(mOriginalClientBinder, args);
}
}
从上述代码可以看到是拦截了Binder.queryLocalInterface并改写内部实现,通过动态代理方式对本地代理对象进行一次包装,这样startActivity等方法都会转移到mBinderInvocationHandler也就是AMSInterceptHandler中了
接下来看看AMSInterceptHandler
可以看到tinker对startActivity相关方法进行拦截处理了,内部实现核心就是偷梁换柱
private Object handleStartActivity(Object target, Method method, Object[] args) throws Throwable {
int intentIdx = -1;
for (int i = 0; i < args.length; ++i) {
if (args[i] instanceof Intent) {
intentIdx = i;
break;
}
}
if (intentIdx != -1) {
// 构建新的Intent存储旧信息
final Intent newIntent = new Intent((Intent) args[intentIdx]);
// 将跳转的Activity替换为占位Activity
processActivityIntent(newIntent);
// 替换已有的intent
args[intentIdx] = newIntent;
}
return method.invoke(target, args);
}
private void processActivityIntent(Intent intent) {
String origPackageName = null;
String origClassName = null;
// 1. 解析组件包名、跳转的组件名(activity)
if (intent.getComponent() != null) {
origPackageName = intent.getComponent().getPackageName();
origClassName = intent.getComponent().getClassName();
} else {
ResolveInfo rInfo = mContext.getPackageManager().resolveActivity(intent, 0);
if (rInfo == null) {
rInfo = IncrementComponentManager.resolveIntent(intent);
}
if (rInfo != null && rInfo.filter != null && rInfo.filter.hasCategory(Intent.CATEGORY_DEFAULT)) {
origPackageName = rInfo.activityInfo.packageName;
origClassName = rInfo.activityInfo.name;
}
}
// 2. 如果跳转的组件缺失是来自补丁包中组件,说明需要替换
if (IncrementComponentManager.isIncrementActivity(origClassName)) {
final ActivityInfo origInfo = IncrementComponentManager.queryActivityInfo(origClassName);
final boolean isTransparent = hasTransparentTheme(origInfo);
// 找一个合适的占位activity
final String stubClassName = ActivityStubManager.assignStub(origClassName, origInfo.launchMode, isTransparent);
// 在tinker_iek_old_component中保存原始组件名,以便于后面替换回来
// 同时跳转的组件名替换为占位组件名
storeAndReplaceOriginalComponentName(intent, origPackageName, origClassName, stubClassName);
}
}
private void storeAndReplaceOriginalComponentName(Intent intent, String origPackageName, String origClassName, String stubClassName) {
final ComponentName origComponentName = new ComponentName(origPackageName, origClassName);
ShareIntentUtil.fixIntentClassLoader(intent, mContext.getClassLoader());
intent.putExtra(EnvConsts.INTENT_EXTRA_OLD_COMPONENT, origComponentName);
final ComponentName stubComponentName = new ComponentName(origPackageName, stubClassName);
intent.setComponent(stubComponentName);
}
总结如下:
1. 提取出跳转的原始activity组件名称、包名
2. 如果原始组件名称是补丁包中,寻找一个合适StubActivity替换它
3. 在newIntent中存储以key为**tinker_iek_old_component**存储originCompoentName()
到了这里其实对于组件activity的修复目前已经完成了一半工作,接下来就是对于AMS通知app LaunActivity的事件拦截
对于拦截LaunchActivity事件,tinker其实也是针对不同版本做了兼容处理
对于8.1之前是hook ActivityThread.mH对象中的callBack
类HandlerMessageInterceptor就是将mH对象的中的mCallBack通过动态代理方式包装一层以达到拦截LaunchActivity目的
MHMessageHandler.handleActivity可以看到对LAUNCH_ACTIVITY事件做拦截,可以猜测内部应该是把上面说的tinker_iek_old_component中存的值取出来设置为真正跳转组件,因为它才是存储我们实际需要跳转的组件信息(RealActivity)
public boolean handleMessage(Message msg) {
int what = msg.what;
if (what == LAUNCH_ACTIVITY) {
try {
final Object activityClientRecord = msg.obj;
if (activityClientRecord == null) {
ShareTinkerLog.w(TAG, "msg: [" + msg.what + "] has no 'obj' value.");
return false;
}
final Field intentField = ShareReflectUtil.findField(activityClientRecord, "intent");
// 1. 从ActivityClientRecord中获取intent
final Intent maybeHackedIntent = (Intent) intentField.get(activityClientRecord);
if (maybeHackedIntent == null) {
ShareTinkerLog.w(TAG, "cannot fetch intent from message received by mH.");
return false;
}
ShareIntentUtil.fixIntentClassLoader(maybeHackedIntent, mContext.getClassLoader());
// 2. 从intent中找到真正需要跳转的组件信息
final ComponentName oldComponent = maybeHackedIntent.getParcelableExtra(EnvConsts.INTENT_EXTRA_OLD_COMPONENT);
if (oldComponent == null) {
ShareTinkerLog.w(TAG, "oldComponent was null, start " + maybeHackedIntent.getComponent() + " next.");
return false;
}
final Field activityInfoField = ShareReflectUtil.findField(activityClientRecord, "activityInfo");
final ActivityInfo aInfo = (ActivityInfo) activityInfoField.get(activityClientRecord);
if (aInfo == null) {
return false;
}
final ActivityInfo targetAInfo = IncrementComponentManager.queryActivityInfo(oldComponent.getClassName());
if (targetAInfo == null) {
ShareTinkerLog.e(TAG, "Failed to query target activity's info,"
+ " perhaps the target is not hotpluged component. Target: " + oldComponent.getClassName());
return false;
}
// 一些兼容性处理
fixActivityScreenOrientation(activityClientRecord, targetAInfo.screenOrientation);
// 补充activityInfo信息
fixStubActivityInfo(aInfo, targetAInfo);
// 3. 替换组件
maybeHackedIntent.setComponent(oldComponent);
// 4. 数据清理
maybeHackedIntent.removeExtra(EnvConsts.INTENT_EXTRA_OLD_COMPONENT);
} catch (Throwable thr) {
ShareTinkerLog.e(TAG, "exception in handleMessage.", thr);
}
}
return false;
}
Android8.1 (27)开始tinker则是通过自定义TinkerHackInstrumentation替换ActivityThread.mInstrumentaion对象来实现拦截activity的创建的,这里有一个疑问在27版本中mH中其实是存在LAUNCH_ACTIVITY的消息事件的,28开始mH中没有LAUNCH_ACTIVITY的消息事件,没明白为什么tinker判断条件是系统版本27而不是28
上图不难猜测processIntent的操作和MHMessageHandler.handleActivity中的操作类似,将占位的组件替换为真正的组件调整从而实现偷梁换柱,代码比较简单,直接上图吧
至此对Tinker的组件热修复已解析完毕
这里mAppLike就是Sample工程中的SampleApplicationLike这里就是接入Tinker的一些初始化代码了,有兴趣同学可以自行研究下,此处贴下Sample的示例代码
至此我们对Tinker的初始化及运行时的实现原理有了更深入的理解