Tinker
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的热补丁修复框架,热修复 必定绕过不二部分
- class修复,在Android中也就是dex修复
- 资源修复
二、Tinker在启动做了什么?
先看下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);
}
- 设置了Tinker异常处理器,当有异常出现时记录堆栈信息并持久化到本地
- 调用onBaseContextAttached
- loadTinker
- 调用delegateClassName对象即SampleApplicationLike.onBaseContextAttached方法
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);
if (useSafeMode) {
ShareTinkerInternals.setSafeModeCount(this, 0);
}
} catch (TinkerRuntimeException e) {
throw e;
} catch (Throwable thr) {
throw new TinkerRuntimeException(thr.getMessage(), thr);
}
}
loadTinker是其tinker启动加载核心;内部其实就是寻找并解压补丁包,并动态加载
三、loadTinker
我们看到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();
tryLoadPatchFilesInternal(app, resultIntent);
long cost = SystemClock.elapsedRealtime() - begin;
ShareIntentUtil.setIntentPatchCostTime(resultIntent, cost);
return resultIntent;
}
tryLoadPatchFilesInternal内部实现有330行代码左右,但主流程非常清晰,我们以功能来划分逐部拆解即可,主要分为四个小块
- 读取补丁信息
- patch版本相关处理
- dex,res,so等校验处理
- load patch dex
1. 解析补丁信息
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;
}
File patchDirectoryFile = SharePatchFileUtil.getPatchDirectory(app);
if (patchDirectoryFile == null) {
ShareTinkerLog.w(TAG, "tryLoadPatchFiles:getPatchDirectory == null");
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_DIRECTORY_NOT_EXIST);
return;
}
String patchDirectoryPath = patchDirectoryFile.getAbsolutePath();
if (!patchDirectoryFile.exists()) {
ShareTinkerLog.w(TAG, "tryLoadPatchFiles:patch dir not exist:" + patchDirectoryPath);
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_DIRECTORY_NOT_EXIST);
return;
}
File patchInfoFile = SharePatchFileUtil.getPatchInfoFile(patchDirectoryPath);
if (!patchInfoFile.exists()) {
ShareTinkerLog.w(TAG, "tryLoadPatchFiles:patch info not exist:" + patchInfoFile.getAbsolutePath());
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_INFO_NOT_EXIST);
return;
}
File patchInfoLockFile = SharePatchFileUtil.getPatchInfoLockFile(patchDirectoryPath);
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
2. patch版本处理
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) {
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);
if (isRemoveNewVersion) {
ShareTinkerLog.w(TAG, "found clean patch mark and we are in main process, delete patch file now.");
if (patchName != null) {
final boolean isNewVersionLoadedBefore = oldVersion.equals(newVersion);
if (isNewVersionLoadedBefore) {
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) {
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;
}
String patchName = SharePatchFileUtil.getPatchVersionDirectory(version);
if (patchName == null) {
ShareTinkerLog.w(TAG, "tryLoadPatchFiles:patchName is null");
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_VERSION_DIRECTORY_NOT_EXIST);
return;
}
String patchVersionDirectory = patchDirectoryPath + "/" + patchName;
File patchVersionDirectoryFile = new File(patchVersionDirectory);
if (!patchVersionDirectoryFile.exists()) {
ShareTinkerLog.w(TAG, "tryLoadPatchFiles:onPatchVersionDirectoryNotFound");
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_VERSION_DIRECTORY_NOT_EXIST);
return;
}
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");
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());
}
3. 校验
private void tryLoadPatchFilesInternal(TinkerApplication app, Intent resultIntent) {
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");
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());
}
上述一大坨代码,都是一些清理,和补丁包apk的校验操作
assets/so_meta.txt格式
$name,$path,$md5,$rawCrc,$pathMd5
对应ShareBsDiffPatchInfo类
assets/res_meta.txt格式 ==> ShareResPatchInfo
$arscBasseCrc,$resArscMd5 //firstLine
Dex校验
final boolean isEnabledForDex = ShareTinkerInternals.isTinkerEnabledForDex(tinkerFlag);
final boolean isArkHotRuning = ShareTinkerInternals.isArkHotRuning();
if (!isArkHotRuning && isEnabledForDex) {
boolean dexCheck = TinkerDexLoader.checkComplete(patchVersionDirectory, securityCheck, oatDex, resultIntent);
if (!dexCheck) {
ShareTinkerLog.w(TAG, "tryLoadPatchFiles:dex check fail");
return;
}
}
先说下dex_meta.txt配置文件格式
$name,$path,$destMd5InDvm,$dexMd5InArt,$dexDiffMd5,$oldDexCrc,$newDexCrc,$dexMode
接下来逐步分析如何校验dex文件
public static boolean checkComplete(String directory, ShareSecurityCheck securityCheck, String oatDir, Intent intentResult) {
String meta = securityCheck.getMetaContentMap().get(DEX_MEAT_FILE);
if (meta == null) {
return true;
}
LOAD_DEX_LIST.clear();
classNDexInfo.clear();
ArrayList<ShareDexDiffPatchInfo> allDexInfo = new ArrayList<>();
ShareDexDiffPatchInfo.parseDexDiffPatchInfo(meta, allDexInfo);
if (allDexInfo.isEmpty()) {
return true;
}
HashMap<String, String> dexes = new HashMap<>();
ShareDexDiffPatchInfo testInfo = null;
for (ShareDexDiffPatchInfo info : allDexInfo) {
if (isJustArtSupportDex(info)) {
continue;
}
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;
}
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 {
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, "");
}
String dexDirectory = directory + "/" + DEX_PATH + "/";
File dexDir = new File(dexDirectory);
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);
for (String name : dexes.keySet()) {
File dexFile = new File(dexDirectory + name);
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;
}
File dexOptFile = new File(SharePatchFileUtil.optimizedPathFor(dexFile, optimizeDexDirectoryFile));
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;
}
}
intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_DEXES_PATH, dexes);
return true;
}
这部操作总结如下:
- 解析dex_meta.txt配置文件
- 从配置文件解析出的dex进行文件校验及odex文件校验(本次属于快速校验,所以没有对每个dex做完整性校验)
华为方舟编译器校验
final boolean isEnabledForArkHot = ShareTinkerInternals.isTinkerEnabledForArkHot(tinkerFlag);
if (isArkHotRuning && isEnabledForArkHot) {
boolean arkHotCheck = TinkerArkHotLoader.checkComplete(patchVersionDirectory, securityCheck, resultIntent);
if (!arkHotCheck) {
ShareTinkerLog.w(TAG, "tryLoadPatchFiles:dex check fail");
return;
}
}
SO校验
final boolean isEnabledForNativeLib = ShareTinkerInternals.isTinkerEnabledForNativeLib(tinkerFlag);
if (isEnabledForNativeLib) {
boolean libCheck = TinkerSoLoader.checkComplete(patchVersionDirectory, securityCheck, resultIntent);
if (!libCheck) {
ShareTinkerLog.w(TAG, "tryLoadPatchFiles:native lib check fail");
return;
}
}
so库校验其实就是解析so_meta.tx文件中信息以校验meta中记载的文件是否存在
so_meta.tx每行格式如下(对应ShareBsDiffPatchInfo类)
$name,$path,$md5,$rawCrc,$pathMd5
资源校验
final boolean isEnabledForResource = ShareTinkerInternals.isTinkerEnabledForResource(tinkerFlag);
ShareTinkerLog.w(TAG, "tryLoadPatchFiles:isEnabledForResource:" + isEnabledForResource);
if (isEnabledForResource) {
boolean resourceCheck = TinkerResourceLoader.checkComplete(app, patchVersionDirectory, securityCheck, resultIntent);
if (!resourceCheck) {
ShareTinkerLog.w(TAG, "tryLoadPatchFiles:resource check fail");
return;
}
}
总结:资源校验其实分为二个步骤
- 资源完整性校验
- 资源是否可以下发修复校验
1. 资源完整性校验
public static boolean checkComplete(Context context, String directory, ShareSecurityCheck securityCheck, Intent intentResult) {
String meta = securityCheck.getMetaContentMap().get(RESOURCE_META_FILE);
if (meta == null) {
return true;
}
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);
if (!SharePatchFileUtil.isLegalFile(resourceFile)) {
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_RESOURCE_FILE_NOT_EXIST);
return false;
}
try {
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;
}
这步主要做了如下:
- 解析res_meta.txt文件以判断resArscMd5是否合法(长度为32即可)
- 校验resources.apk是否存在且可读
assets/res_meta.txt格式如下 ==> ShareResPatchInfo
$arscBasseCrc,$resArscMd5 //firstLine
2. 校验资源是否可以修复
TinkerResourcePatcher.isResourceCanPatch(context)
-
4.4-7.0)获取取android.app.ResourcesManage.getInstance()中的mActiveResources属性, -
7.0及以后开始获取mActiveResources属性 -
4.4以前获取android.app.ActivityThread中的mActiveResources
如果获取属性失败,则会抛出异常,说明资源不能patch下发
编译模式
之所以要说这个是,在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不同版本启用模式差异如下
private void tryLoadPatchFilesInternal(TinkerApplication app, Intent resultIntent) {
boolean isSystemOTA = ShareTinkerInternals.isVmArt()
&& ShareTinkerInternals.isSystemOTA(patchInfo.fingerPrint)
&& Build.VERSION.SDK_INT >= 21 && !ShareTinkerInternals.isAfterAndroidO();
resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_SYSTEM_OTA, isSystemOTA);
if (mainProcess) {
if (versionChanged) {
patchInfo.oldVersion = version;
}
if (oatModeChanged) {
patchInfo.oatDir = oatDex;
patchInfo.isRemoveInterpretOATDir = true;
}
}
if (!checkSafeModeCount(app)) {
if (mainProcess) {
patchInfo.oldVersion = "";
patchInfo.newVersion = "";
patchInfo.isRemoveNewVersion = false;
SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, patchInfo, patchInfoLockFile);
ShareTinkerInternals.killProcessExceptMain(app);
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 {
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);
}
}
}
4. load patch dex
if (!isArkHotRuning && isEnabledForDex) {
boolean loadTinkerJars = TinkerDexLoader.loadTinkerJars(app, patchVersionDirectory, oatDex, resultIntent, isSystemOTA, isProtectedApp);
if (isSystemOTA) {
...
}
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;
}
}
这部分主要做了如下操作
- 非方舟编译器调用TinkerDexLoader.loadTinkerJars加载jar
- jar加载成功,针对ota的系统,更新patch.fingerPrint、otaDir等
- 如果是运行在方舟编译器中,单独使用TinkerArkHotLoader.loadTinkerArkHot做处理
我们先看看loadTinkerJars吧
public static boolean loadTinkerJars(final TinkerApplication application, String directory, String oatDir, Intent intentResult, boolean isSystemOTA, boolean isProtectedApp) {
if (LOAD_DEX_LIST.isEmpty() && classNDexInfo.isEmpty()) {
ShareTinkerLog.w(TAG, "there is no dex to load");
return true;
}
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) {
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);
if (!SharePatchFileUtil.verifyDexFileMd5(file, checkMd5)) {
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));
}
legalFiles.add(file);
}
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) {
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);
}
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);
deleteOutOfDateOATFile(directory);
intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_INTERPRET_EXCEPTION, throwable);
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_GET_OTA_INSTRUCTION_SET_EXCEPTION);
return false;
}
deleteOutOfDateOATFile(directory);
ShareTinkerLog.w(TAG, "systemOTA, try parallel oat dexes, targetISA:" + targetISA);
optimizeDir = new File(directory + "/" + INTERPRET_DEX_OPTIMIZE_PATH);
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) {
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();
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;
}
- dex文件的完整性校验
- 如果系统是ota升级,则清理oat相关文件,并以解释模式,进行dex2oat优化
- 安装dex(7.0开始使用NewClassLoader来规避混合编译带来的问题,之前版本使用常规方法,将dex插入原ClassLoader.pathList.dexElements列表最前面)
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;
if (Build.VERSION.SDK_INT >= 24 && !isProtectedApp) {
classLoader = NewClassLoaderInjector.inject(application, loader, dexOptDir, useDLC, files);
} else {
injectDexesInternal(classLoader, files, dexOptDir);
}
sPatchDexCount = files.size();
ShareTinkerLog.i(TAG, "after loaded classloader: " + classLoader + ", dex size:" + sPatchDexCount);
if (!checkDexInstall(classLoader)) {
SystemClassLoaderAdder.uninstallPatchDex(classLoader);
throw new TinkerRuntimeException(ShareConstants.CHECK_DEX_INSTALL_FAIL);
}
}
}
四、补丁Dex的加载
7.0之前dex加载
7.0之前加载dex比较简单,这里以6.0为例
从上图可以看到读取loader中的pathList(DexPathList)中dexElements,热修复的dex会插入到dexElements数组前面,这样就达到热修复目的,为什么?这涉及到类加载机制了
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
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;
}
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
7.0及以后dex加载
先了解下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及实现注入的
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();
}
final ClassLoader newClassLoader = createNewClassLoader(oldClassLoader,
dexOptDir, useDLC, true, patchedDexPaths);
doInject(app, newClassLoader);
return newClassLoader;
}
-
创建新的classLoader并迁移数据
- 先找到老的so相关库列表
将需要加载的patchDex路径,及原so库路径传递给新classLoader -
注入自定义classLoader
- 替换Application.mBase对象(ContextImpl)的mClassLoader
- 替换mBase对象中的**mPackageInfo(LoaderAPK)**的mClassLoader
- 8.1以前还需要替换app.getResources()的mClassLoader、mDrawableInflater对象的mClassLoader
通过自定义代理ClassLoader实现了运行时先从代理ClassLoader加载类,后从原始ClassLoader加载,以解决混合编译模式下热修复失效问题
五、补丁资源的加载
代码
if (isEnabledForResource) {
boolean loadTinkerResources = TinkerResourceLoader.loadTinkerResources(app, patchVersionDirectory, resultIntent);
if (!loadTinkerResources) {
ShareTinkerLog.w(TAG, "tryLoadPatchFiles:onPatchLoadResourcesFail");
return;
}
}
资源加载相对来说比较简单
-
资源映射表文件resources.arsc md5校验 -
TinkerResourcePatcher.monkeyPatchExistingResources
- 修改ActivityThread中mPackages、mResourcepackages中LoaderApk中的resDir为新的资源包路径
- 调用新建的newAssetmanager.addAssetPath方法将资源路径告知系统
- 对于N开始版本处理下分享库(动态添加)
- 对于resources中的assets字段统一用newAssetmanger对象替换
- 其他一些兼容性问题
贴下代码
public static void monkeyPatchExistingResources(Context context, String externalResourceFile) throws Throwable {
if (externalResourceFile == null) {
return;
}
final ApplicationInfo appInfo = context.getApplicationInfo();
final Field[] packagesFields;
if (Build.VERSION.SDK_INT < 27) {
packagesFields = new Field[]{packagesFiled, resourcePackagesFiled};
} else {
packagesFields = new Field[]{packagesFiled};
}
for (Field field : packagesFields) {
final Object value = field.get(currentActivityThread);
for (Map.Entry<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);
}
}
}
if (((Integer) addAssetPathMethod.invoke(newAssetManager, externalResourceFile)) == 0) {
throw new IllegalStateException("Could not create new AssetManager");
}
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);
}
}
if (stringBlocksField != null && ensureStringBlocksMethod != null) {
stringBlocksField.set(newAssetManager, null);
ensureStringBlocksMethod.invoke(newAssetManager);
}
for (WeakReference<Resources> wr : references) {
final Resources resources = wr.get();
if (resources == null) {
continue;
}
try {
assetsFiled.set(resources, newAssetManager);
} catch (Throwable ignore) {
final Object resourceImpl = resourcesImplFiled.get(resources);
final Field implAssets = findField(resourceImpl, "mAssets");
implAssets.set(resourceImpl, newAssetManager);
}
clearPreloadTypedArrayIssue(resources);
resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics());
}
if (Build.VERSION.SDK_INT >= 24) {
try {
if (publicSourceDirField != null) {
publicSourceDirField.set(context.getApplicationInfo(), externalResourceFile);
}
} catch (Throwable ignore) {
}
}
if (!checkResUpdate(context)) {
throw new TinkerRuntimeException(ShareConstants.CHECK_RES_INSTALL_FAIL);
}
}
六、组件热修复
入口代码
if ((isEnabledForDex || isEnabledForArkHot) && isEnabledForResource) {
ComponentHotplug.install(app, securityCheck);
}
原理
目前Tinker只支持Android四大组件中的Activity的热修复,Java本身是就具备动态化能力,在Android要启动一个Activity,AMS其实会对这个Activity做校验,AMS是单独的一个进程,我们无法做到hook AMS校验能力;通常的解决方式是偷梁换柱
- 预先声明一个占位StubActivity
- 在startActivity时,将真实跳转的Activity替换为StubActivity,以绕过AMS的校验机制
- hook AMS通知app时启动Activity的事件,将StubActivity替换为真实跳转的Activity
而tinker其实也是这么做的,只是实现方式比网上很多实现的更优雅些
实现
public synchronized static void install(TinkerApplication app, ShareSecurityCheck checker) throws UnsupportedEnvironmentException {
if (!sInstalled) {
try {
if (IncrementComponentManager.init(app, checker)) {
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();
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);
}
}
}
1. 解析补丁包中的activity组件信息
IncrementComponentManager.init方法就是用来干这事的,它主要从assets/inc_component_meta.txt文件中解析组件Activity信息
从解析组件代码来看,目前Tinker只支持组件activity,暂不支持service,receiver,provider组件热修复
2. hook AMS的代理对象
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 {
return (IBinder) sGetServiceMethod.invoke(null, mServiceName);
}
包装远程binder引用
@Override
protected IBinder decorate(IBinder target) throws Throwable {
if (target == null) {
throw new IllegalStateException("target is null.");
}
if (ITinkerHotplugProxy.class.isAssignableFrom(target.getClass())) {
return target;
} else {
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";
}
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
3. 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) {
final Intent newIntent = new Intent((Intent) args[intentIdx]);
processActivityIntent(newIntent);
args[intentIdx] = newIntent;
}
return method.invoke(target, args);
}
private void processActivityIntent(Intent intent) {
String origPackageName = null;
String origClassName = null;
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;
}
}
if (IncrementComponentManager.isIncrementActivity(origClassName)) {
final ActivityInfo origInfo = IncrementComponentManager.queryActivityInfo(origClassName);
final boolean isTransparent = hasTransparentTheme(origInfo);
final String stubClassName = ActivityStubManager.assignStub(origClassName, origInfo.launchMode, isTransparent);
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的事件拦截
4. AMS通知app LaunchActivity的事件拦截
对于拦截LaunchActivity事件,tinker其实也是针对不同版本做了兼容处理
入口代码
8.1之前处理方式
对于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");
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());
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);
fixStubActivityInfo(aInfo, targetAInfo);
maybeHackedIntent.setComponent(oldComponent);
maybeHackedIntent.removeExtra(EnvConsts.INTENT_EXTRA_OLD_COMPONENT);
} catch (Throwable thr) {
ShareTinkerLog.e(TAG, "exception in handleMessage.", thr);
}
}
return false;
}
8.1及之后处理方式
Android8.1 (27)开始tinker则是通过自定义TinkerHackInstrumentation替换ActivityThread.mInstrumentaion对象来实现拦截activity的创建的,这里有一个疑问在27版本中mH中其实是存在LAUNCH_ACTIVITY的消息事件的,28开始mH中没有LAUNCH_ACTIVITY的消息事件,没明白为什么tinker判断条件是系统版本27而不是28
上图不难猜测processIntent的操作和MHMessageHandler.handleActivity中的操作类似,将占位的组件替换为真正的组件调整从而实现偷梁换柱,代码比较简单,直接上图吧
至此对Tinker的组件热修复已解析完毕
七、mAppLike.onBaseContextAttached
这里mAppLike就是Sample工程中的SampleApplicationLike这里就是接入Tinker的一些初始化代码了,有兴趣同学可以自行研究下,此处贴下Sample的示例代码
至此我们对Tinker的初始化及运行时的实现原理有了更深入的理解
|