前言
对于重要类的分析已经全部加上了详细注释,代码在github.com/ColorfulHor…
上一篇文章中我们大致分析了补丁包的合成过程,本文将逐步分析补丁的加载过程。补丁加载主要是dex和资源文件的加载,对于dex文件来说,加载实际上就是将补丁dex转换为Element插入到app的PathClassLoader.pathList.dexElements数组最前面,对于dex加载过程不了解的读者可以先去看看相关知识或者源码;对于资源加载,主要是替换掉原本的AssetManager,相对没那么复杂;另外对于新增activity的处理涉及到hook相关内容,需要对activity的启动过程有一些了解。
Application隔离
在看补丁加载过程之前,我们要先了解一下tinker中的application隔离,Tinker要求将Application类和其他逻辑类隔离开,为此特意用了AnnotationProcessor插件生成Application类,然后将生命周期代理到ApplicationLike类,Application对于开发者不可见,所有逻辑代码都放入ApplicationLike,避免了Application对于其他类的引用。对于Tinker如何生成Application不做具体分析,因为生成的Application并没有起到什么实际上的作用,有兴趣可以自行翻看相关源码。
进行Application隔离的原因
由于Android N之后由于混合编译会出现加载补丁以后地址错乱崩溃问题,所以Tinker在Application中使用自定义ClassLoader替换掉了PathClassLoader来加载后续类;而Application类本身必定先被PathClassLoader加载,所以如果Application类中引用了其他逻辑类,这个类也会先被原PathClassLoader加载,如果后面这个类又被替换后的自定义ClassLoader加载,使用时就会出现ClassCastException问题。
补丁加载的时机和大致流程
集成Tinker后,对于开发者来说应用入口变成了ApplicationLike类,但是实际上的应用入口还是Application类,Application类中先进行补丁的加载,然后再启动ApplicationLike,所以补丁加载的逻辑在TinkerApplication类中。TinkerApplication主要做了几件事,简单列一下:
attachBaseContext时调用loadTinker方法加载补丁,这一步会替换掉PathClassLoadercreateInlineFence创建TinkerApplicationInlineFence实例,此类用于阻止Art下内联带来的影响- 通过TinkerApplicationInlineFence回调ApplicationLike各个生命周期方法
public abstract class TinkerApplication extends Application {
protected void onBaseContextAttached(Context base, long applicationStartElapsedTime, long applicationStartMillisTime) {
try {
// 反射调用loader类的tryLoad方法
// 因为开发者可以自定义拓展loader,所以根据ApplicationLike中配置的loader类名反射调用
// 加载补丁,下面单独分析
loadTinker();
// loadTinker已经将app PathClassLoader替换,这里是替换后的cl
mCurrentClassLoader = base.getClassLoader();
mInlineFence = createInlineFence(this, tinkerFlags, delegateClassName,
tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime,
tinkerResultIntent);
// 回调ApplicationLike onBaseContextAttached
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);
}
}
}
TinkerApplicationInlineFence阻止内联
方法内联可以简单理解为,由于被调用的方法过于简单,其中的代码直接被拷贝到调用处,避免压入方法栈这一优化行为。
如果在Application类中直接调用ApplicationLike中方法,ApplicationLike方法编译时被内联到Application,而执行时由于替换了原PathClassLoader,导致加载Application和ApplicationLike的ClassLoader是不同的,此时在Android P开始就会出现被内联的代码和内联到的类的classloader不一致的校验错误,Android P以下,>=N的系统又会出现ClassCastException。
为了阻止对于ApplicationLike的内联,将TinkerApplicationInlineFence作为调用中间层,通过特殊命名加上try代码块阻止内联。
private Handler createInlineFence(Application app,
int tinkerFlags,
String delegateClassName,
boolean tinkerLoadVerifyFlag,
long applicationStartElapsedTime,
long applicationStartMillisTime,
Intent resultIntent) {
try {
// 使用替换后的classLoader反射ApplicationLike类
final Class<?> delegateClass = Class.forName(delegateClassName, false, mCurrentClassLoader);
final Constructor<?> constructor = delegateClass.getConstructor(Application.class, int.class, boolean.class,
long.class, long.class, Intent.class);
// 创建ApplicationLike实例
final Object appLike = constructor.newInstance(app, tinkerFlags, tinkerLoadVerifyFlag,
applicationStartElapsedTime, applicationStartMillisTime, resultIntent);
// 反射创建TinkerApplicationInlineFence
final Class<?> inlineFenceClass = Class.forName(
"com.tencent.tinker.entry.TinkerApplicationInlineFence", false, mCurrentClassLoader);
final Class<?> appLikeClass = Class.forName(
"com.tencent.tinker.entry.ApplicationLike", false, mCurrentClassLoader);
final Constructor<?> inlineFenceCtor = inlineFenceClass.getConstructor(appLikeClass);
inlineFenceCtor.setAccessible(true);
return (Handler) inlineFenceCtor.newInstance(appLike);
} catch (Throwable thr) {
throw new TinkerRuntimeException("createInlineFence failed", thr);
}
}
public final class TinkerApplicationInlineFence extends Handler {
private final ApplicationLike mAppLike;
public TinkerApplicationInlineFence(ApplicationLike appLike) {
mAppLike = appLike;
}
@Override
public void handleMessage(Message msg) {
handleMessage_$noinline$(msg);
}
// 特殊命名方法
private void handleMessage_$noinline$(Message msg) {
// try块阻止内联
try {
dummyThrowExceptionMethod();
} finally {
handleMessageImpl(msg);
}
}
......
private static void dummyThrowExceptionMethod() {
if (TinkerApplicationInlineFence.class.isPrimitive()) {
throw new RuntimeException();
}
}
}
加载补丁预操作
首先在TinkerApplication.loadTinker反射调用TinkerLoader.tryLoad,最后调用到TinkerLoader.tryLoadPatchFilesInternal,此方法主要是进行加载前一系列的校验。
private void loadTinker() {
try {
// 因为loader类可以被开发者自定义,所以反射创建tinker loader实例,默认为TinkerLoader类
Class<?> tinkerLoadClass = Class.forName(loaderClassName, false, TinkerApplication.class.getClassLoader());
// 调用TinkerLoader tryLoad
Method loadMethod = tinkerLoadClass.getMethod(TINKER_LOADER_METHOD, TinkerApplication.class);
Constructor<?> constructor = tinkerLoadClass.getConstructor();
tinkerResultIntent = (Intent) loadMethod.invoke(constructor.newInstance(), this);
} catch (Throwable e) {
//has exception, put exception error code
tinkerResultIntent = new Intent();
ShareIntentUtil.setIntentReturnCode(tinkerResultIntent, ShareConstants.ERROR_LOAD_PATCH_UNKNOWN_EXCEPTION);
tinkerResultIntent.putExtra(INTENT_PATCH_EXCEPTION, e);
}
}
public class TinkerLoader extends AbstractTinkerLoader {
public Intent tryLoad(TinkerApplication app) {
ShareTinkerLog.d(TAG, "tryLoad test test");
// intent记录加载补丁的信息
Intent resultIntent = new Intent();
long begin = SystemClock.elapsedRealtime();
tryLoadPatchFilesInternal(app, resultIntent);
long cost = SystemClock.elapsedRealtime() - begin;
ShareIntentUtil.setIntentPatchCostTime(resultIntent, cost);
return resultIntent;
}
}
TinkerLoader.tryLoadPatchFilesInternal中先对补丁进行一系列的校验,然后校验合成后的dex、so库、资源文件的完整性合法性,最后依次调用TinkerDexLoader、TinkerSoLoader、TinkerResourceLoader加载补丁。
另外方法内包含对于系统OTA升级后解释模式运行的相关逻辑,这里先单独拿出来说,以免看加载补丁逻辑时感到混乱。
系统OTA后加载补丁ANR的相关处理
android 8.0之前动态加载的dex会被以speed模式全量编译,系统OTA更新后导致旧的补丁dex的oat文件失效,此时运行app就会对它重新执行dex2oat,可能耗时比较久造成ANR。所以TinkerLoader中加载补丁前先判断是否OTA后首次运行,然后对补丁dex先做quiken/interpret-only模式的dex2oat操作,以解释模式运行,之后再进行后台的全量编,大致流程如下。
- TinkerLoader加载补丁时判断此次加载是否系统OTA升级后首次运行,是的话调用
TinkerDexOptimizer.optimizeAll以解释模式对合成后dex做dexopt,保存在data/data/包名/tinker/patch-xxx/interpret文件夹。加载odex,然后将patch.info和resultIntent中oatDir设为interpet - 补丁加载完成后,
Tinker.install时调用TinkerLoadResult.parseTinkerResult解析intentResult,判断oatDir为interpet则回调DefaultPatchListener.onLoadInterpret,之后再次调用到UpgradePatch.tryPatch,重走一遍补丁合成流程重新触发后台dex2oat,同时将patch.info中oatDir设为changing,以便下次加载补丁时不再以解释模式加载 - 下次加载补丁时TinkerLoader中判断oatDir为changing,代表oat文件已经生成,不再需要以解释模式加载补丁,此时标记patch.info中isRemoveInterpretOATDir为true,下次加载删除data/data/包名/tinker/patch-xxx/interpret文件夹
下面我们从代码入手,大致看一下加载补丁前的准备工作,之后进一步分析真正的加载过程
public class TinkerLoader extends AbstractTinkerLoader {
private void tryLoadPatchFilesInternal(TinkerApplication app, Intent resultIntent) {
.....补丁目录以及patch.info文件是否存在等校验
// 读取patch.info文件中记录的信息
patchInfo = SharePatchInfo.readAndCheckPropertyWithLock(patchInfoFile, patchInfoLockFile);
if (patchInfo == null) {
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_INFO_CORRUPTED);
return;
}
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) {
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_INFO_CORRUPTED);
return;
}
// 是否主进程(app运行的进程)
boolean mainProcess = ShareTinkerInternals.isInMainProcess(app);
boolean isRemoveNewVersion = patchInfo.isRemoveNewVersion;
if (mainProcess) {
final String patchName = SharePatchFileUtil.getPatchVersionDirectory(newVersion);
// 清除补丁操作不是及时生效,而是等到下一次加载时生效
// 是否清除所有补丁,若补丁被加载过需要重置patch.info并杀死主进程以外所有进程
if (isRemoveNewVersion) {
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;
}
}
}
// 是否删除解释编译产生的odex文件
if (patchInfo.isRemoveInterpretOATDir) {
// 重写patch.info删除odex标识,然后杀死其他进程并且删除odex文件
patchInfo.isRemoveInterpretOATDir = false;
SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, patchInfo, patchInfoLockFile);
ShareTinkerInternals.killProcessExceptMain(app);
String patchVersionDirFullPath = patchDirectoryPath + "/" + patchName;
// data/data/包名/tinker/patch-xxx/interpet
SharePatchFileUtil.deleteDir(patchVersionDirFullPath + "/" + ShareConstants.INTERPRET_DEX_OPTIMIZE_PATH);
}
}
resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_OLD_VERSION, oldVersion);
resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_NEW_VERSION, newVersion);
// oldVersion和newVersion不同说明合成了新补丁,但是还没有加载过
boolean versionChanged = !(oldVersion.equals(newVersion));
// changing代表后台dex2oat已经完成,切换到非解释模式
// 此时使用dex2oat生成的odex文件,删除解释模式的odex文件
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)) {
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_INFO_BLANK);
return;
}
// 待加载的补丁名:patch-641e634c
String patchName = SharePatchFileUtil.getPatchVersionDirectory(version);
// 待加载的补丁路径 data/data/包名/tinker/patch-641e634c
String patchVersionDirectory = patchDirectoryPath + "/" + patchName;
File patchVersionDirectoryFile = new File(patchVersionDirectory);
final String patchVersionFileRelPath = SharePatchFileUtil.getPatchVersionFile(version);
// 补丁文件:data/data/包名/tinker/patch-md5/patch-md5.apk
File patchVersionFile = (patchVersionFileRelPath != null ? new File(patchVersionDirectoryFile.getAbsolutePath(), patchVersionFileRelPath) : null);
// 此类用于读取补丁包中的meta文件进行校验
ShareSecurityCheck securityCheck = new ShareSecurityCheck(app);
// 校验补丁的TinkerId、签名、MD5、meta文件等的合法性
int returnCode = ShareTinkerInternals.checkTinkerPackage(app, tinkerFlag, patchVersionFile, securityCheck);
resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_PACKAGE_CONFIG, securityCheck.getPackagePropertiesIfPresent());
// 是否合成dex
final boolean isEnabledForDex = ShareTinkerInternals.isTinkerEnabledForDex(tinkerFlag);
// 是否方舟编译器
final boolean isArkHotRuning = ShareTinkerInternals.isArkHotRuning();
if (!isArkHotRuning && isEnabledForDex) {
// 解析dex_meta,校验要加载的dex以及对应的的odex是否存在
boolean dexCheck = TinkerDexLoader.checkComplete(patchVersionDirectory, securityCheck, oatDex, resultIntent);
if (!dexCheck) {
return;
}
}
final boolean isEnabledForNativeLib = ShareTinkerInternals.isTinkerEnabledForNativeLib(tinkerFlag);
if (isEnabledForNativeLib) {
// 校验so库合法性
boolean libCheck = TinkerSoLoader.checkComplete(patchVersionDirectory, securityCheck, resultIntent);
if (!libCheck) {
return;
}
}
final boolean isEnabledForResource = ShareTinkerInternals.isTinkerEnabledForResource(tinkerFlag);
ShareTinkerLog.w(TAG, "tryLoadPatchFiles:isEnabledForResource:" + isEnabledForResource);
if (isEnabledForResource) {
// 校验补丁资源正确性
boolean resourceCheck = TinkerResourceLoader.checkComplete(app, patchVersionDirectory, securityCheck, resultIntent);
if (!resourceCheck) {
return;
}
}
// 判断系统是否进行了OTA升级,系统OTA后首次启动需要先以解释模式运行
// 8.0以后不需要做此操作
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"));
return;
} else {
// 非主进程将 patchInfo isRemoveNewVersion设为true,下次在主进程删除
ShareTinkerInternals.cleanPatch(app);
}
}
if (!isArkHotRuning && isEnabledForDex) {
// 加载dex,isSystemOTA = true以解释模式加载
boolean loadTinkerJars = TinkerDexLoader.loadTinkerJars(app, patchVersionDirectory, oatDex, resultIntent, isSystemOTA, isProtectedApp);
if (isSystemOTA) {
// update fingerprint after load success
patchInfo.fingerPrint = Build.FINGERPRINT;
// ota后第一次启动加载补丁成功,oatDir = interpet
patchInfo.oatDir = loadTinkerJars ? ShareConstants.INTERPRET_DEX_OPTIMIZE_PATH : ShareConstants.DEFAULT_DEX_OPTIMIZE_PATH;
oatModeChanged = false;
if (!SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, patchInfo, patchInfoLockFile)) {
return;
}
// intent中设置oatDir为interpret
resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_OAT_DIR, patchInfo.oatDir);
}
if (!loadTinkerJars) {
return;
}
}
if (isEnabledForResource) {
// 加载资源文件
boolean loadTinkerResources = TinkerResourceLoader.loadTinkerResources(app, patchVersionDirectory, resultIntent);
if (!loadTinkerResources) {
return;
}
}
// TODO
if ((isEnabledForDex || isEnabledForArkHot) && isEnabledForResource) {
ComponentHotplug.install(app, securityCheck);
}
if (!AppInfoChangedBlocker.tryStart(app)) {
ShareTinkerLog.w(TAG, "tryLoadPatchFiles:AppInfoChangedBlocker install fail.");
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_BAIL_HACK_FAILURE);
return;
}
// Before successfully exit, we should update stored version info and kill other process
// to make them load latest patch when we first applied newer one.
if (mainProcess && (versionChanged || oatModeChanged)) {
//update old version to new
if (!SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, patchInfo, patchInfoLockFile)) {
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_REWRITE_PATCH_INFO_FAIL);
ShareTinkerLog.w(TAG, "tryLoadPatchFiles:onReWritePatchInfoCorrupted");
return;
}
ShareTinkerInternals.killProcessExceptMain(app);
}
//all is ok!
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_OK);
ShareTinkerLog.i(TAG, "tryLoadPatchFiles: load end, ok!");
}
}
加载补丁
加载补丁部分重点分析dex加载、资源文件加载以及新增Activity相关处理。
dex加载
dex加载主要逻辑在TinkerDexLoader.loadTinkerJars方法中,此方法中先进行一些校验,然后调用到SystemClassLoaderAdder.installDexes方法进行加载,如果是系统OTA后首次启动则需要删除旧的oat文件,然后解释模式执行dex2oat再加载。
public class TinkerDexLoader {
// dalvik下需要加载的dex
private static final ArrayList<ShareDexDiffPatchInfo> LOAD_DEX_LIST = new ArrayList<>();
// art下需要加载的dex
private static HashSet<ShareDexDiffPatchInfo> classNDexInfo = new HashSet<>();
public static boolean loadTinkerJars(final TinkerApplication application, String directory,
String oatDir, Intent intentResult, boolean isSystemOTA, boolean isProtectedApp) {
if (LOAD_DEX_LIST.isEmpty() && classNDexInfo.isEmpty()) {
return true;
}
ClassLoader classLoader = TinkerDexLoader.class.getClassLoader();
if (classLoader != null) {
} else {
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_CLASSLOADER_NULL);
return false;
}
String dexPath = directory + "/" + DEX_PATH + "/";
// 需要加载的dex集合
ArrayList<File> legalFiles = new ArrayList<>();
for (ShareDexDiffPatchInfo info : LOAD_DEX_LIST) {
// dalvik下跳过没有改变的非主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);
// 校验合成后dex MD5和dex_meta中md5
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;
}
}
legalFiles.add(file);
}
if (isVmArt && !classNDexInfo.isEmpty()) {
// art下所有dex合成为tinker_classN.apk然后dex2oat生成了tinker_classN.odex
// 合成后的tinker_classN.apk,art下加载时会自动寻找相应的tinker_classN.odex文件
File classNFile = new File(dexPath + ShareConstants.CLASS_N_APK_NAME);
long start = System.currentTimeMillis();
if (application.isTinkerLoadVerifyFlag()) {
for (ShareDexDiffPatchInfo info : classNDexInfo) {
// 校验合成后dex MD5和dex_meta中md5
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;
}
}
}
legalFiles.add(classNFile);
}
File optimizeDir = new File(directory + "/" + oatDir);
if (isSystemOTA) {
final boolean[] parallelOTAResult = {true};
final Throwable[] parallelOTAThrowable = new Throwable[1];
String targetISA;
try {
// 获取cpu指令集
targetISA = ShareTinkerInternals.getCurrentInstructionSet();
} catch (Throwable 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;
}
// 删除失效的oat文件
deleteOutOfDateOATFile(directory);
// data/data/包名/tinker/patch-xxx/interpret
optimizeDir = new File(directory + "/" + INTERPRET_DEX_OPTIMIZE_PATH);
// 解释模式dex2oat
TinkerDexOptimizer.optimizeAll(......);
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();
// inject classloader 开始加载
SystemClassLoaderAdder.installDexes(application, classLoader, optimizeDir, legalFiles, isProtectedApp, useDLC);
} catch (Throwable e) {
intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, e);
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_LOAD_EXCEPTION);
return false;
}
return true;
}
}
SystemClassLoaderAdder.installDexes方法中根据判断系统版本,在android7.0及以上版本时调用NewClassLoaderInjector.inject创建新PathClassLoader替换app原有PathClassLoader,避免android7.0之后以混合编译与对热补丁影响,之前我们提到过。而对于7.0以下版本,直接调用SystemClassLoaderAdder.injectDexesInternal方法,将补丁dex插入到app PathClassLoader的pathList.dexElements最前面。加载完成后通过TinkerTestDexLoad.isPatch判断补丁是否加载成功,TinkerTestDexLoad.isPatch在loader中为false,加载补丁后被test.dex中类覆盖为true。
public class SystemClassLoaderAdder {
// 判断补丁是否加载成功的类
public static final String CHECK_DEX_CLASS = "com.tencent.tinker.loader.TinkerTestDexLoad";
public static final String CHECK_DEX_FIELD = "isPatch";
public static void installDexes(Application application, ClassLoader loader, File dexOptDir, List<File> files,
boolean isProtectedApp, boolean useDLC) throws Throwable {
if (!files.isEmpty()) {
files = createSortedAdditionalPathEntries(files);
ClassLoader classLoader = loader;
if (Build.VERSION.SDK_INT >= 24 && !isProtectedApp) {
// 7.0之后创建新ClassLoader替换原ClassLoader避免混合编译带来的问题
classLoader = NewClassLoaderInjector.inject(application, loader, dexOptDir, useDLC, files);
} else {
// 加固也不替换ClassLoader
// android7.0之前直接将补丁dex插入原ClassLoader中pathList中dexElements最前面
injectDexesInternal(classLoader, files, dexOptDir);
}
sPatchDexCount = files.size();
// 反射TinkerTestDexLoad.isPatch判断是否加载成功
if (!checkDexInstall(classLoader)) {
SystemClassLoaderAdder.uninstallPatchDex(classLoader);
throw new TinkerRuntimeException(ShareConstants.CHECK_DEX_INSTALL_FAIL);
}
}
}
}
补丁dex插入
SystemClassLoaderAdder.injectDexesInternal方法中根据android版本不同采取不同方法插入补丁dex,我们简单看一下V23.install。
private static final class V23 {
private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
File optimizedDirectory)
throws IllegalArgumentException, IllegalAccessException,
NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
// 获取BaseDexClassLoader.pathList
Field pathListField = ShareReflectUtil.findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
// makePathElements反射调用DexPathList.makePathElements/makeDexElements得到Element数组
// 将补丁dex生成的Element数组插入DexPathList.dexElements
ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", makePathElements(dexPathList,
new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
suppressedExceptions));
if (suppressedExceptions.size() > 0) {
for (IOException e : suppressedExceptions) {
ShareTinkerLog.w(TAG, "Exception in makePathElement", e);
throw e;
}
}
}
}
ClassLoader替换
在上一篇文章中已经提到过加载时ClassLoader替换,NewClassLoaderInjector.createNewClassLoader根据版本和配置不同创建相应的代理ClassLoader,然后调用NewClassLoaderInjector.doInject替换App中各处classLoader实例为代理ClassLoader,使得运行时先从代理ClassLoader加载补丁中类,后从原始ClassLoader加载。
private static ClassLoader createNewClassLoader(ClassLoader oldClassLoader,
File dexOptDir,
boolean useDLC,
String... patchDexPaths) throws Throwable {
// 反射获取BaseDexClassLoader的DexPathList字段
// old oldClassLoader是当前app的PathClassLoader
final Field pathListField = findField(
Class.forName("dalvik.system.BaseDexClassLoader", false, oldClassLoader),
"pathList");
final Object oldPathList = pathListField.get(oldClassLoader);
final StringBuilder dexPathBuilder = new StringBuilder();
final boolean hasPatchDexPaths = patchDexPaths != null && patchDexPaths.length > 0;
if (hasPatchDexPaths) {
for (int i = 0; i < patchDexPaths.length; ++i) {
if (i > 0) {
dexPathBuilder.append(File.pathSeparator);
}
dexPathBuilder.append(patchDexPaths[i]);
}
}
// 拼接需要dex2oat的dex文件路径
final String combinedDexPath = dexPathBuilder.toString();
// 反射DexPathList中nativeLibraryDirectories字段,so库路径
final Field nativeLibraryDirectoriesField = findField(oldPathList.getClass(), "nativeLibraryDirectories");
......
// 拼接so库路径
final String combinedLibraryPath = libraryPathBuilder.toString();
ClassLoader result = null;
if (useDLC && Build.VERSION.SDK_INT >= 27) {
// https://developer.android.google.cn/reference/dalvik/system/DelegateLastClassLoader
// https://www.androidos.net.cn/android/10.0.0_r6/xref/libcore/dalvik/src/main/java/dalvik/system/DelegateLastClassLoader.java
// DelegateLastClassLoader是android8.1后新增的,继承于PathClassLoader,实行最后查找策略
result = new DelegateLastClassLoader(combinedDexPath, combinedLibraryPath, ClassLoader.getSystemClassLoader());
// 将之前的PathClassLoader设为创建的DelegateLastClassLoader的双亲
final Field parentField = ClassLoader.class.getDeclaredField("parent");
parentField.setAccessible(true);
parentField.set(result, oldClassLoader);
} else {
// 做的事情和DelegateLastClassLoader差不多
result = new TinkerClassLoader(combinedDexPath, dexOptDir, combinedLibraryPath, oldClassLoader);
}
// Android8.0之前版本替换原本的PathClassLoader中PathList中的classLoader为新创建的
// Android 8.0之后不支持多个类加载器同时使用同一个DexFile对象来定义类,所以不能替换
if (Build.VERSION.SDK_INT < 26) {
findField(oldPathList.getClass(), "definingContext").set(oldPathList, result);
}
return result;
}
private static void doInject(Application app, ClassLoader classLoader) throws Throwable {
Thread.currentThread().setContextClassLoader(classLoader);
// ContextWrapper.mBase是该app的ContextImpl实例,LoadedApk.makeApplication中创建
final Context baseContext = (Context) findField(app.getClass(), "mBase").get(app);
try {
// 替换ContextImpl中mClassLoader
findField(baseContext.getClass(), "mClassLoader").set(baseContext, classLoader);
} catch (Throwable ignored) {
// 8.0前ContextImpl中没有mClassLoader
}
// app的ContextImpl中mPackageInfo是一个LoadedApk实例
final Object basePackageInfo = findField(baseContext.getClass(), "mPackageInfo").get(baseContext);
// 替换LoadedApk的ClassLoader
findField(basePackageInfo.getClass(), "mClassLoader").set(basePackageInfo, classLoader);
if (Build.VERSION.SDK_INT < 27) {
final Resources res = app.getResources();
try {
// 替换Resources中ClassLoader
findField(res.getClass(), "mClassLoader").set(res, classLoader);
final Object drawableInflater = findField(res.getClass(), "mDrawableInflater").get(res);
if (drawableInflater != null) {
// 替换DrawableInflater中ClassLoader
findField(drawableInflater.getClass(), "mClassLoader").set(drawableInflater, classLoader);
}
} catch (Throwable ignored) {
// Ignored.
}
}
}
资源文件加载
加载资源包逻辑并不复杂,主要逻辑在TinkerResourceLoader.loadTinkerResources方法中,此处先校验arsc文件md5,然后调用TinkerResourcePatcher.monkeyPatchExistingResources方法真正开始加载资源包,主要流程如下。
- 将原app中LoadedApk实例中mResDir字段设为新资源包路径
- 创建新的AssetManager实例,并设置它的资源路径为新资源包路径
- 将Resources.mAssets设为新创建的AssetManager,清除原app的Resources中typedArray缓存,刷新资源配置
有个地方要注意,设置新的AssetManager资源路径时,在7.0后系统并且app使用了共享资源库的情况下还需调用AssetManager.addAssetPathAsSharedLibrary。
ApplicationInfo.sharedLibraryFiles存储app用到的共享资源库,共享资源库就是像so库一样,可以将资源包共享给其他应用使用。
如果app用到了共享资源库,那么可能会遇到修复热修后 SharedLibrary R 类中的资源 ID 与 AssetManager 中 Package ID 不一致导致的资源找不到问题。
这里将补丁资源包也添加为共享资源库。
参考
#1372
Android资源管理中的SharedLibrary
public class TinkerResourceLoader {
public static boolean loadTinkerResources(TinkerApplication application, String directory, Intent intentResult) {
if (resPatchInfo == null || resPatchInfo.resArscMd5 == null) {
return true;
}
// data/data/包名/tinker/patch-xxx/res/resources.apk
String resourceString = directory + "/" + RESOURCE_PATH + "/" + RESOURCE_FILE;
File resourceFile = new File(resourceString);
if (application.isTinkerLoadVerifyFlag()) {
// 校验arsc文件md5
if (!SharePatchFileUtil.checkResourceArscMd5(resourceFile, resPatchInfo.resArscMd5)) {
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_RESOURCE_MD5_MISMATCH);
return false;
}
}
try {
// 加载资源包
TinkerResourcePatcher.monkeyPatchExistingResources(application, resourceString);
} catch (Throwable e) {
// 加载资源失败移除补丁
try {
SystemClassLoaderAdder.uninstallPatchDex(application.getClassLoader());
} catch (Throwable throwable) {
ShareTinkerLog.e(TAG, "uninstallPatchDex failed", e);
}
intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, e);
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_RESOURCE_LOAD_EXCEPTION);
return false;
}
return true;
}
}
class TinkerResourcePatcher {
private static final String TAG = "Tinker.ResourcePatcher";
private static final String TEST_ASSETS_VALUE = "only_use_to_test_tinker_resource.txt";
// ResourcesManager中保存的Resources弱引用集合
private static Collection<WeakReference<Resources>> references = null;
private static Object currentActivityThread = null;
// 新资源包的AssetManager
private static AssetManager newAssetManager = null;
// AssetManager.addAssetPath
private static Method addAssetPathMethod = null;
// AssetManager.addAssetPathAsSharedLibrary >=android7.0需要调用
private static Method addAssetPathAsSharedLibraryMethod = null;
// AssetManager.mStringBlocks字段
private static Field stringBlocksField = null;
// AssetManager.ensureStringBlocks
private static Method ensureStringBlocksMethod = null;
// Resources.mAssets
private static Field assetsFiled = null;
// Resources.mResourcesImpl
private static Field resourcesImplFiled = null;
// LoadedApk.mResDir
private static Field resDir = null;
// ActivityThread.mPackages
private static Field packagesFiled = null;
// ActivityThread.mResourcePackages
private static Field resourcePackagesFiled = null;
// ApplicationInfo.publicSourceDir
private static Field publicSourceDirField = null;
public static void monkeyPatchExistingResources(Context context, String externalResourceFile) throws Throwable {
// data/data/包名/tinker/patch-xxx/res/resources.apk
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};
}
// 遍历activityThread中的LoadedApk
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);
// 找到原app的LoadedApk实例,将它的mResDir设为新资源包路径
if (appInfo.sourceDir.equals(resDirPath)) {
resDir.set(loadedApk, externalResourceFile);
}
}
}
// newAssetManager为新创建的AssetManager,调用AssetManager.addAssetPath设置它的路径为新资源包
if (((Integer) addAssetPathMethod.invoke(newAssetManager, externalResourceFile)) == 0) {
throw new IllegalStateException("Could not create new AssetManager");
}
// >=android7.0并且使用了共享资源库的情况下还需要调用addAssetPathAsSharedLibrary
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");
}
}
}
// 重新创建资源字符串索引
if (stringBlocksField != null && ensureStringBlocksMethod != null) {
stringBlocksField.set(newAssetManager, null);
ensureStringBlocksMethod.invoke(newAssetManager);
}
// 遍历ResourcesManager中Resources
for (WeakReference<Resources> wr : references) {
final Resources resources = wr.get();
if (resources == null) {
continue;
}
try {
// 将Resources.mAssets设为新创建的AssetManager
assetsFiled.set(resources, newAssetManager);
} catch (Throwable ignore) {
// android7.0以后该字段为Resources.mResourcesImpl.mAssets
final Object resourceImpl = resourcesImplFiled.get(resources);
final Field implAssets = findField(resourceImpl, "mAssets");
implAssets.set(resourceImpl, newAssetManager);
}
// 清除Resources中typedArray缓存
clearPreloadTypedArrayIssue(resources);
// 内部调用AssetManager.setConfiguration,刷新资源配置
resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics());
}
// 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
// android7.0之后,如果activity包含webView,屏幕旋转后补丁资源会失效
if (Build.VERSION.SDK_INT >= 24) {
try {
if (publicSourceDirField != null) {
// 重设ApplicationInfo.publicSourceDir
publicSourceDirField.set(context.getApplicationInfo(), externalResourceFile);
}
} catch (Throwable ignore) {
}
}
// 以类似test.dex的方式检查补丁资源是否加载成功
if (!checkResUpdate(context)) {
throw new TinkerRuntimeException(ShareConstants.CHECK_RES_INSTALL_FAIL);
}
}
}
添加新增Activity
新增activity由于没有在manifest中注册,需要用hook的方式绕过AMS的检测才能够启动,大致原理:
-
在原app中预先注册一些代理activity
-
跳转到新增activity时拦截startActivity过程,通过动态代理修改调用参数,将target activity相关参数替换为代理activity的,以通过AMS检查
-
拦截AMS对于客户端真正启动activity的调用处,再将代理activity修改回target activity 关于hook相关知识可以通过这篇博客简单了解,原理都差不多,hook点小有区别。
tinker中hook大致流程如下: -
解析补丁包inc_component_meta.txt,将activity节点解析为ActivityInfo对象并保存
-
hook ServiceManager,
ServiceBinderInterceptor.fetchTarget方法获取AMS客户端代理对象(BpBinder) -
ServiceBinderInterceptor.decorate创建该BpBinder的动态代理,通过AMSInterceptHandlerhook对于AMS.startActivity的调用,将target activity替换为tinker loader预先注册的代理activity -
ServiceBinderInterceptor.inject将ServiceManager.sCache中AMS代理替换为hook上一步创建的动态代理对象 -
同理hook PMS中一些方法,防止Activity校验出错
-
8.1以下系统通过hook
ActivityThread.mH.mCallBack,在系统调用handleLaunchActivity之前将target activity替换回来 -
8.1及以上系统通过hook
ActivityThread.mInstrumentation,将它替换为TinkerHackInstrumentation,修改相关方法实现将target activity替换回来。
ComponentHotplug.install开始进行hook
ServiceBinderInterceptor和HandlerMessageInterceptor都是Interceptor的实现类,Interceptor.fetchTarget获取需要hook的实例,Interceptor.decorate创建该对象的动态代理,Interceptor.inject使用动态代理替换原实例。
public final class ComponentHotplug {
public synchronized static void install(TinkerApplication app, ShareSecurityCheck checker) throws UnsupportedEnvironmentException {
if (!sInstalled) {
try {
// 解析inc_component_meta,将xml activity节点解析为ActivityInfo对象并存
if (IncrementComponentManager.init(app, checker)) {
// ServiceManager.getService获取AMS客户端代理对象,然后创建此代理对象的动态代理对象,hook startActivity等方法
sAMSInterceptor = new ServiceBinderInterceptor(app, EnvConsts.ACTIVITY_MANAGER_SRVNAME, new AMSInterceptHandler(app));
// 同理hook PMS
sPMSInterceptor = new ServiceBinderInterceptor(app, EnvConsts.PACKAGE_MANAGER_SRVNAME, new PMSInterceptHandler());
sAMSInterceptor.install();
sPMSInterceptor.install();
if (Build.VERSION.SDK_INT < 27) {
// android 8.1以下
// ActivityThread.mH
final Handler mH = fetchMHInstance(app);
// hook ActivityThread.mH,将H.mCallBack替换为MHMessageHandler
sMHMessageInterceptor = new HandlerMessageInterceptor(mH, new MHMessageHandler(app));
sMHMessageInterceptor.install();
} else {
// >=8.1 hook ActivityThread.mInstrumentation,把他替换为TinkerHackInstrumentation
sTinkerHackInstrumentation = TinkerHackInstrumentation.create(app);
sTinkerHackInstrumentation.install();
}
sInstalled = true;
}
} catch (Throwable thr) {
uninstall();
throw new UnsupportedEnvironmentException(thr);
}
}
}
}
AMSInterceptHandler替换activity
拦截所有调用AMS启动activity的方法,将target activity替换为代理
public class AMSInterceptHandler implements BinderInvocationHandler {
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) {
// target activity intent
final Intent newIntent = new Intent((Intent) args[intentIdx]);
// 替换activity
processActivityIntent(newIntent);
args[intentIdx] = newIntent;
}
return method.invoke(target, args);
}
private void processActivityIntent(Intent intent) {
// 原始activity包名和类名
String origPackageName = null;
String origClassName = null;
if (intent.getComponent() != null) {
origPackageName = intent.getComponent().getPackageName();
origClassName = intent.getComponent().getClassName();
} else {
......
}
if (IncrementComponentManager.isIncrementActivity(origClassName)) {
final ActivityInfo origInfo = IncrementComponentManager.queryActivityInfo(origClassName);
final boolean isTransparent = hasTransparentTheme(origInfo);
// 根据将要跳转的activity信息得到tinker预先注册的代理activity类名
// tinker loader manifest预先注册了一些activity,比如ActivityStubs$STDStub_00
final String stubClassName = ActivityStubManager.assignStub(origClassName, origInfo.launchMode, isTransparent);
// 用代理activity替换原activity
storeAndReplaceOriginalComponentName(intent, origPackageName, origClassName, stubClassName);
}
}
private void storeAndReplaceOriginalComponentName(Intent intent, String origPackageName, String origClassName, String stubClassName) {
final ComponentName origComponentName = new ComponentName(origPackageName, origClassName);
// 设置用于解析parcel的classLoader
ShareIntentUtil.fixIntentClassLoader(intent, mContext.getClassLoader());
// intent bundle中添加原始componentName信息,以便AMS处理完成后再替换回来
intent.putExtra(EnvConsts.INTENT_EXTRA_OLD_COMPONENT, origComponentName);
// intent设置componentName为代理activity的
final ComponentName stubComponentName = new ComponentName(origPackageName, stubClassName);
intent.setComponent(stubComponentName);
}
}
MHMessageHandler还原activity
hook ActivityThread.mH.mCallBack,让它先调用到MessageHandler.handleMessage
public class MHMessageHandler implements MessageHandler {
@Override
public boolean handleMessage(Message msg) {
int what = msg.what;
if (what == LAUNCH_ACTIVITY) {
try {
final Object activityClientRecord = msg.obj;
if (activityClientRecord == null) {
return false;
}
// 反射ActivityClientRecord.intent获取之前startActivity传给ams的intent
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());
// 获取原始activity ComponentName
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");
// 代理activityInfo
final ActivityInfo aInfo = (ActivityInfo) activityInfoField.get(activityClientRecord);
if (aInfo == null) {
return false;
}
// 原始activityInfo
final ActivityInfo targetAInfo = IncrementComponentManager.queryActivityInfo(oldComponent.getClassName());
if (targetAInfo == null) {
return false;
}
// 由于代理activity没有screenOrientation信息,这里需要还原target Activity相关信息
fixActivityScreenOrientation(activityClientRecord, targetAInfo.screenOrientation);
// target activityInfo字段copy到代理activityInfo,并替换componentName为target的
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;
}
}
TinkerHackInstrumentation还原activity
TinkerHackInstrumentation继承Instrumentation,通过反射将ActivityThread中Instrumentation替换完成hook,除了hook点不同外其他逻辑和AMSInterceptHandler差不多,这里就不贴代码了。
后记
加载补丁这部分通过大量的反射对于framework层进行了Hack,随着安卓版本的变迁以及高版本权限的收紧,以后将会越来越难适配,兼容性也难以保证,对于维护者来说道阻且长。
三篇文章完结,自此对于Tinker源码分析告一段落,我自己算是小有收获,也希望能帮到读者。