通过上一篇文章《tinker热修复——dex补丁加载过程》,基本上我们已经熟悉了tinker加载dex的过程,tinker除了能够新增类,还可以新增资源和so库等,那么tinker是如何做到热更资源的呢?资源补丁的加载过程又是怎么样的呢?
我们都知道,tinker打补丁包的时候,只会打diff的补丁包,也就说补丁包中包括资源的diff,而生成资源的diff的时候,会把变化的索引写在补丁包的assets/res_meta.txt中,当补丁下发到app后,会将所有的资源整合起来生成一个resources.apk(该资源包,包含新增的资源和原来的资源),在加载资源补丁时候就是加载这个包含所有资源的包,然后将LoadApk中的资源路径替换成补丁的路径,并将Resources容器中的属性都替换成新的加载补丁的AssertManager。那么接下来我们看看tinker是如何一步步加载资源补丁的。
在《tinker热修复——dex补丁加载过程》文章中知道,最终调用的加载过程都是在TinkerLoader的tryLoadPatchFilesInternal方法中的。那么我们重点关注该方法,我们注意到经过对补丁包的一层层的安全校验,检查资源补丁的时候,会调用TinkerResourceLoader的checkComplete方法。
/**
* resource file exist?
* fast check, only check whether exist
*
* @param directory
* @return boolean
*/
public static boolean checkComplete(Context context, String directory, ShareSecurityCheck securityCheck, Intent intentResult) {
//拿到补丁包中res_meta.txt的信息
String meta = securityCheck.getMetaContentMap().get(RESOURCE_META_FILE);
//not found resource
if (meta == null) {
return true;
}
/**
* only parse first line for faster
* 将meta中第一行的数据读取到resPatchInfo中用来快速校验.
*/
ShareResPatchInfo.parseResPatchInfoFirstLine(meta, resPatchInfo);
if (resPatchInfo.resArscMd5 == null) {
return true;
}
//checkResPatchInfo:检查asrc文件的MD5本身是否为空或者长度是否合法
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) {
Log.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的信息,并把meta中的第一行的数据读取到resPatchInfo中,做一个快速检验,然后检查文件的md5是否有效,以及检查资源补丁的文件是否有效,最后调用TinkerResourcePatcher的isResourceCanPatch方法。
/**
* 获取资源更新时需要的method和Field
* 并且保存起来,方便补丁加载时使用
* @param context
* @throws Throwable
*/
public static void isResourceCanPatch(Context context) throws Throwable {
// - Replace mResDir to point to the external resource file instead of the .apk. This is
// used as the asset path for new Resources objects.
// - Set Application#mLoadedApk to the found LoadedApk instance
/**
*
* Find the ActivityThread instance for the current thread
* 获取当前线程的ActivityThread对象
*/
Class<?> activityThread = Class.forName("android.app.ActivityThread");
currentActivityThread = ShareReflectUtil.getActivityThread(context, activityThread);
/**
*
* API version 8 has PackageInfo, 10 has LoadedApk. 9, I don't know.
* LoadedApk是从Android 2.3开始才有的
* 该类是对之前系统ActivityThread的内部类PackageInfo重新封装而成的.所以这里要分开处理.
*/
Class<?> loadedApkClass;
try {
loadedApkClass = Class.forName("android.app.LoadedApk");
} catch (ClassNotFoundException e) {
loadedApkClass = Class.forName("android.app.ActivityThread$PackageInfo");
}
//获取loadedApkClass的mResDir属性,设置成可访问
resDir = loadedApkClass.getDeclaredField("mResDir");
resDir.setAccessible(true);
//获取activityThread的mPackages属性,设置成可访问
packagesFiled = activityThread.getDeclaredField("mPackages");
packagesFiled.setAccessible(true);
//获取activityThread的mResourcePackages属性,设置可访问
resourcePackagesFiled = activityThread.getDeclaredField("mResourcePackages");
resourcePackagesFiled.setAccessible(true);
/**
*
* Create a new AssetManager instance and point it to the resources
* 创建一个AssetManager实例
* 一些ROM修改了类名,如Baidu的ROM改成了android.content.res.BaiduAssetManager
* 所以要做兼容
*/
AssetManager assets = context.getAssets();
// Baidu os
if (assets.getClass().getName().equals("android.content.res.BaiduAssetManager")) {
Class baiduAssetManager = Class.forName("android.content.res.BaiduAssetManager");
newAssetManager = (AssetManager) baiduAssetManager.getConstructor().newInstance();
} else {
newAssetManager = AssetManager.class.getConstructor().newInstance();
}
//获取AssetManager的addAssetPath方法,设置成可访问
addAssetPathMethod = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
addAssetPathMethod.setAccessible(true);
/**
*
* Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm
* in L, so we do it unconditionally.
* 在Kitkat需要调用AssetManager的ensureStringBlocks方法
* 在Lollipop则不需要调用
* 但是不用区分系统,照常调用,不会造成任何问题
*/
ensureStringBlocksMethod = AssetManager.class.getDeclaredMethod("ensureStringBlocks");
ensureStringBlocksMethod.setAccessible(true);
/**
* 加载补丁要替换的Resources对象在KITKAT以下是以HashMap的类型作为ActivityThread类的属性.
* 其余的系统版本都是以ArrayMap被ResourcesManager持有的.
* 需要做系统区分
*/
// Iterate over all known Resources objects
//获取所有的Resources对象的引用
if (SDK_INT >= KITKAT) {
//pre-N
// Find the singleton instance of ResourcesManager
Class<?> resourcesManagerClass = Class.forName("android.app.ResourcesManager");
Method mGetInstance = resourcesManagerClass.getDeclaredMethod("getInstance");
mGetInstance.setAccessible(true);
//调用getInstance方法,拿到ResourcesManager对象
Object resourcesManager = mGetInstance.invoke(null);
try {
//获取ResourcesManager的mActiveResources对象,设置可访问
Field fMActiveResources = resourcesManagerClass.getDeclaredField("mActiveResources");
fMActiveResources.setAccessible(true);
//获取得到所有Resources对象的引用
ArrayMap<?, WeakReference<Resources>> activeResources19 =
(ArrayMap<?, WeakReference<Resources>>) fMActiveResources.get(resourcesManager);
references = activeResources19.values();
} catch (NoSuchFieldException ignore) {
// N moved the resources to mResourceReferences
Field mResourceReferences = resourcesManagerClass.getDeclaredField("mResourceReferences");
mResourceReferences.setAccessible(true);
references = (Collection<WeakReference<Resources>>) mResourceReferences.get(resourcesManager);
}
} else {
Field fMActiveResources = activityThread.getDeclaredField("mActiveResources");
fMActiveResources.setAccessible(true);
HashMap<?, WeakReference<Resources>> activeResources7 =
(HashMap<?, WeakReference<Resources>>) fMActiveResources.get(currentActivityThread);
references = activeResources7.values();
}
// check resource,校验是否为null
if (references == null) {
throw new IllegalStateException("resource references is null");
}
// fix jianGuo pro has private field 'mAssets' with Resource
// try use mResourcesImpl first
//将Resources对象中持有AssetManager对象引用的属性mAssets,hook出来
//mAssets是加载补丁时进行替换的
if (SDK_INT >= 24) {
//Android N的路径变成了Resources -> ResourcesImpl -> AssetManager
try {
// N moved the mAssets inside an mResourcesImpl field
resourcesImplFiled = Resources.class.getDeclaredField("mResourcesImpl");
resourcesImplFiled.setAccessible(true);
} catch (Throwable ignore) {
// for safety
assetsFiled = Resources.class.getDeclaredField("mAssets");
assetsFiled.setAccessible(true);
}
} else {
assetsFiled = Resources.class.getDeclaredField("mAssets");
assetsFiled.setAccessible(true);
}
// final Resources resources = context.getResources();
// isMiuiSystem = resources != null && MIUI_RESOURCE_CLASSNAME.equals(resources.getClass().getName());
try {
publicSourceDirField = ShareReflectUtil.findField(ApplicationInfo.class, "publicSourceDir");
} catch (NoSuchFieldException ignore) {
}
}
复制代码
该方法就是为资源补丁的替换做准备的工作,并且把这些准备的工作保存起来,等到替换的时候用。首先获取ActivityThread和LoadedApk,通过反射获取LoadedApk的mResDir属性,该属性需要设置补丁包的路径。通过ActivityThread获取mPackages和mResourcePackages属性,然后创建新的AssetManager同时通过反射获取AssetManager的addAssetPath方法(通过addAssetPath可以将补丁加载到新的AssetManager中)。
Class<?> resourcesManagerClass = Class.forName("android.app.ResourcesManager");
Method mGetInstance = resourcesManagerClass.getDeclaredMethod("getInstance");
mGetInstance.setAccessible(true);
//调用getInstance方法,拿到ResourcesManager对象
Object resourcesManager = mGetInstance.invoke(null);
复制代码
通过反射调用getInstance方法,拿到ResourcesManager对象,因为该对象持有Resources容器对象,因此通过反射就可以拿到所有的Resources对象,拿到Resources对象就可以将其属性替换为新创建的AssetManager。
这里就是通过反射拿到Resources对象中持有AssetManager对象引用的属性mAssets,而mAssets是加载补丁时需要进行替换的。
publicSourceDirField = ShareReflectUtil.findField(ApplicationInfo.class, "publicSourceDir");
复制代码
最后获取ApplicationInfo的属性publicSourceDir是了解决Android的webview引起的问题,在Android N上,如果一个活动包含一个webview,当屏幕旋转时,资源补丁可能会失去效果。
当全部校验通过,并且为资源补丁更新做好准备之后,调用TinkerResourceLoader的loadTinkerResources方法进行资源补丁更新。
loadTinkerResources:
/**
* Load tinker resources
*/
public static boolean loadTinkerResources(TinkerApplication application, String directory, Intent intentResult) {
//判断是否有资源更新
if (resPatchInfo == null || resPatchInfo.resArscMd5 == null) {
return true;
}
//补丁文件
String resourceString = directory + "/" + RESOURCE_PATH + "/" + RESOURCE_FILE;
File resourceFile = new File(resourceString);
long start = System.currentTimeMillis();
if (application.isTinkerLoadVerifyFlag()) {
//校验文件的md5
if (!SharePatchFileUtil.checkResourceArscMd5(resourceFile, resPatchInfo.resArscMd5)) {
Log.e(TAG, "Failed to load resource file, path: " + resourceFile.getPath() + ", expect md5: " + resPatchInfo.resArscMd5);
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_RESOURCE_MD5_MISMATCH);
return false;
}
Log.i(TAG, "verify resource file:" + resourceFile.getPath() + " md5, use time: " + (System.currentTimeMillis() - start));
}
try {
//加载补丁
TinkerResourcePatcher.monkeyPatchExistingResources(application, resourceString);
Log.i(TAG, "monkeyPatchExistingResources resource file:" + resourceString + ", use time: " + (System.currentTimeMillis() - start));
} catch (Throwable e) {
Log.e(TAG, "install resources failed");
//remove patch dex if resource is installed failed
try {
SystemClassLoaderAdder.uninstallPatchDex(application.getClassLoader());
} catch (Throwable throwable) {
Log.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;
}
复制代码
该方法首先检查资源文件是否有更新,然后找到资源文件路径,并检查md5是否校验通过,再进一步调用TinkerResourcePatcher的monkeyPatchExistingResources方法来加载资源。
/**
* @param context
* @param externalResourceFile
* @throws Throwable
*/
public static void monkeyPatchExistingResources(Context context, String externalResourceFile) throws Throwable {
if (externalResourceFile == null) {
return;
}
for (Field field : new Field[]{packagesFiled, resourcePackagesFiled}) {
//获取当前线程的ActivityThread对象
Object value = field.get(currentActivityThread);
for (Map.Entry<String, WeakReference<?>> entry
: ((Map<String, WeakReference<?>>) value).entrySet()) {
//获取LoadedApk容器的对象
Object loadedApk = entry.getValue().get();
if (loadedApk == null) {
continue;
}
if (externalResourceFile != null) {
//将LoadedApk对象的mResDir属性的值替换成资源补丁包的路径
resDir.set(loadedApk, externalResourceFile);
}
}
}
/**
*
* Create a new AssetManager instance and point it to the resources installed under
* 通过addAssetPath方法,将补丁加载到新的AssetManager中
*/
if (((Integer) addAssetPathMethod.invoke(newAssetManager, externalResourceFile)) == 0) {
throw new IllegalStateException("Could not create new AssetManager");
}
// Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm
// in L, so we do it unconditionally.
ensureStringBlocksMethod.invoke(newAssetManager);
//遍历ResourcesManager持有的Resources容器对象
for (WeakReference<Resources> wr : references) {
Resources resources = wr.get();
//pre-N
if (resources != null) {
// Set the AssetManager of the Resources instance to our brand new one
try {
//替换Resources的属性assetsFiled为新的AssetManager
assetsFiled.set(resources, newAssetManager);
} catch (Throwable ignore) {
// N
Object resourceImpl = resourcesImplFiled.get(resources);
// for Huawei HwResourcesImpl
Field implAssets = ShareReflectUtil.findField(resourceImpl, "mAssets");
implAssets.setAccessible(true);
implAssets.set(resourceImpl, newAssetManager);
}
clearPreloadTypedArrayIssue(resources);
//根据原属性重新更新Resources对象的配置.
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
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);
}
}
复制代码
1、获取当前线程的ActivityThread对象和该对象持有的LoadedApk容器;
2、遍历容器中的LoadedApk对象,替换LoadedApk的mResDir属性为补丁物理路径;
3、通过addAssetPath方法,将补丁加载到新创建的AssetManager中;
4、遍历ResourcesManager持有的Resources容器对象;
5、替换Resources的属性assetsFiled为新的AssetManager;
6、根据原属性重新更新Resources对象的配置;
7、调用checkResUpdate方法,补丁生效校验及卸载;
8、更加详细说明请看文章的代码的注释。
以上就是tinker的资源补丁加载的全过程,本人也是刚刚涉及热修复的,如果哪里讲的不够明白,也可以参考《Android 热修复方案Tinker(四) 资源补丁加载》,该文章讲得更加详细,思路也非常清晰。