在应用中使用和加载资源都是通过context对象的getResource方法,下面先以android 10的代码简单分析下资源加载的流程。
资源加载
在Context中访问资源其实是是通过子类ContextImpl对象来实现的,那么ContextImpl是什么时候创建的呢?在app的启动过程中有个类LoadedApk,这个类中有个方法是makeApplication方法,在这个方法中创建了appContext,代码如下:
static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo) {
return createAppContext(mainThread, packageInfo, null);
}
static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo,
String opPackageName) {
if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, null, 0,
null, opPackageName);
context.setResources(packageInfo.getResources());
return context;
}
我们调用的resource就是在这个方法中设置的,那么回过头看下packageInfo是如何获取到资源的。packageInfo实际上是LoadedApk类型的,LoadedApk中的getResources方法代码如下:
public Resources getResources() {
if (mResources == null) {
final String[] splitPaths;
try {
splitPaths = getSplitPaths(null);
} catch (NameNotFoundException e) {
// This should never fail.
throw new AssertionError("null split not found");
}
mResources = ResourcesManager.getInstance().getResources(null, mResDir,
splitPaths, mOverlayDirs, mApplicationInfo.sharedLibraryFiles,
Display.DEFAULT_DISPLAY, null, getCompatibilityInfo(),
getClassLoader());
}
return mResources;
}
ResourcesManager管理应用的资源,实际的资源由ResourcesManager返回。这里应该就是读取资源的核心操作,我们跟进去看下,代码如下:
public @Nullable Resources getResources(@Nullable IBinder activityToken,
@Nullable String resDir,
@Nullable String[] splitResDirs,
@Nullable String[] overlayDirs,
@Nullable String[] libDirs,
int displayId,
@Nullable Configuration overrideConfig,
@NonNull CompatibilityInfo compatInfo,
@Nullable ClassLoader classLoader) {
try {
Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ResourcesManager#getResources");
final ResourcesKey key = new ResourcesKey(
resDir,
splitResDirs,
overlayDirs,
libDirs,
displayId,
overrideConfig != null ? new Configuration(overrideConfig) : null, // Copy
compatInfo);
classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader();
return getOrCreateResources(activityToken, key, classLoader);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
}
}
为了确保每个apk的资源访问对应一个Resource对象,这里通过ResourcesKey来绑定唯一标志。而ResourcesKey则通过apk的路径,设备的配置和兼容信息等构造出来的。有个参数名字是spiltResDirs,结合google之前推出spilt apk。可以猜测一个spilt apk都有自己独立的资源。最终的资源是通过getOrCreateResources返回。这里应该就是资源的创建地方了吧,代码如下:
private @Nullable Resources getOrCreateResources(@Nullable IBinder activityToken,
@NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
synchronized (this) {
if (DEBUG) {
Throwable here = new Throwable();
here.fillInStackTrace();
Slog.w(TAG, "!! Get resources for activity=" + activityToken + " key=" + key, here);
}
if (activityToken != null) {
final ActivityResources activityResources =
getOrCreateActivityResourcesStructLocked(activityToken);
// Clean up any dead references so they don't pile up.
ArrayUtils.unstableRemoveIf(activityResources.activityResources,
sEmptyReferencePredicate);
// Rebase the key's override config on top of the Activity's base override.
if (key.hasOverrideConfiguration()
&& !activityResources.overrideConfig.equals(Configuration.EMPTY)) {
final Configuration temp = new Configuration(activityResources.overrideConfig);
temp.updateFrom(key.mOverrideConfiguration);
key.mOverrideConfiguration.setTo(temp);
}
ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
if (resourcesImpl != null) {
if (DEBUG) {
Slog.d(TAG, "- using existing impl=" + resourcesImpl);
}
return getOrCreateResourcesForActivityLocked(activityToken, classLoader,
resourcesImpl, key.mCompatInfo);
}
// We will create the ResourcesImpl object outside of holding this lock.
} else {
// Clean up any dead references so they don't pile up.
ArrayUtils.unstableRemoveIf(mResourceReferences, sEmptyReferencePredicate);
// Not tied to an Activity, find a shared Resources that has the right ResourcesImpl
ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
if (resourcesImpl != null) {
if (DEBUG) {
Slog.d(TAG, "- using existing impl=" + resourcesImpl);
}
return getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
}
// We will create the ResourcesImpl object outside of holding this lock.
}
// If we're here, we didn't find a suitable ResourcesImpl to use, so create one now.
ResourcesImpl resourcesImpl = createResourcesImpl(key);
if (resourcesImpl == null) {
return null;
}
// Add this ResourcesImpl to the cache.
mResourceImpls.put(key, new WeakReference<>(resourcesImpl));
final Resources resources;
if (activityToken != null) {
resources = getOrCreateResourcesForActivityLocked(activityToken, classLoader,
resourcesImpl, key.mCompatInfo);
} else {
resources = getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
}
return resources;
}
}
这个方法中有个重要的变量activityToken,我们可以先认为这个变量是null。因为此处创建是在新启动应用时调用的,即此时还是创建application时,还没有创建activity。我们重点关注下activityToken为空的情况,通过key在findResourcesImplForKeyLocked方法中没有找到,就会通过createResourcesImpl来创建ResourcesImpl。创建之后将资源ResourcesImpl以之前的key保存在mResourceImpls中,后续可以更加方便获取。我们重点看下创建资源的方法,createResourcesImpl的代码如下:
private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {
final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration);
daj.setCompatibilityInfo(key.mCompatInfo);
final AssetManager assets = createAssetManager(key);
if (assets == null) {
return null;
}
final DisplayMetrics dm = getDisplayMetrics(key.mDisplayId, daj);
final Configuration config = generateConfig(key, dm);
final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);
return impl;
}
构造Resoureces对象需要先构造一个AssetManager对象,然后把这个对象作为Resouces构造函数的参数即可。AsetManager是个什么东西呢?在应用程序开发时,曾经使用该对象,但是构造该对象的方法使用Resources对象的getAssets()方法,并没有使用构造函数,实际上getAssets()所获得AssetManager对象正是这里创建的,AssetManager其实并不只是访问项目的res/assets目录下面的资源,而是访问res下所有的资源。构造AssetManager对象在createAssetManager方法中,关键代码如下:
protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) {
final AssetManager.Builder builder = new AssetManager.Builder();
// resDir can be null if the 'android' package is creating a new Resources object.
// This is fine, since each AssetManager automatically loads the 'android' package
// already.
if (key.mResDir != null) {
try {
builder.addApkAssets(loadApkAssets(key.mResDir, false /*sharedLib*/,
false /*overlay*/));
} catch (IOException e) {
Log.e(TAG, "failed to add asset path " + key.mResDir);
return null;
}
}
// 省略部分代码
return builder.build();
}
loadApkAssets方法返回ApkAssets对象,然后加入到AssetManager.Builder的mUserApkAssets列表中,最后创建了AssetManager。这里继续往下看下ApkAssets是如何创建的,loadApkAssets的代码如下:
private @NonNull ApkAssets loadApkAssets(String path, boolean sharedLib, boolean overlay)
throws IOException {
final ApkKey newKey = new ApkKey(path, sharedLib, overlay);
ApkAssets apkAssets = null;
if (mLoadedApkAssets != null) {
apkAssets = mLoadedApkAssets.get(newKey);
if (apkAssets != null) {
return apkAssets;
}
}
// Optimistically check if this ApkAssets exists somewhere else.
final WeakReference apkAssetsRef = mCachedApkAssets.get(newKey);
if (apkAssetsRef != null) {
apkAssets = apkAssetsRef.get();
if (apkAssets != null) {
if (mLoadedApkAssets != null) {
mLoadedApkAssets.put(newKey, apkAssets);
}
return apkAssets;
} else {
// Clean up the reference.
mCachedApkAssets.remove(newKey);
}
}
// We must load this from disk.
if (overlay) {
apkAssets = ApkAssets.loadOverlayFromPath(overlayPathToIdmapPath(path),
false /*system*/);
} else {
apkAssets = ApkAssets.loadFromPath(path, false /*system*/, sharedLib);
}
if (mLoadedApkAssets != null) {
mLoadedApkAssets.put(newKey, apkAssets);
}
mCachedApkAssets.put(newKey, new WeakReference<>(apkAssets));
return apkAssets;
}
先在mLoadedApkAssets和apkAssetsRef中寻找,如果没有找到再去创建。这里因为是app刚启动,因此就是调用ApkAssets.loadFromPath直接创建。loadFromPath中最终创建的代码如下:
public static @NonNull ApkAssets loadFromPath(@NonNull String path) throws IOException {
return new ApkAssets(path, false /*system*/, false /*forceSharedLib*/, false /*overlay*/);
}
一路下来最终得到一个ApkAssets实例,在createAssetManager方法中,创建的实例加入到AssetManager.Builder的mUserApkAssets列表中,然后调用build方法最终创建得到一个AssetManager。
public AssetManager build() {
// Retrieving the system ApkAssets forces their creation as well.
// 获取系统的ApkAsset。
final ApkAssets[] systemApkAssets = getSystem().getApkAssets();
final int totalApkAssetCount = systemApkAssets.length + mUserApkAssets.size();
final ApkAssets[] apkAssets = new ApkAssets[totalApkAssetCount];
System.arraycopy(systemApkAssets, 0, apkAssets, 0, systemApkAssets.length);
final int userApkAssetCount = mUserApkAssets.size();
for (int i = 0; i < userApkAssetCount; i++) {
apkAssets[i + systemApkAssets.length] = mUserApkAssets.get(i);
}
// Calling this constructor prevents creation of system ApkAssets, which we took care
// of in this Builder.
final AssetManager assetManager = new AssetManager(false /*sentinel*/);
assetManager.mApkAssets = apkAssets;
AssetManager.nativeSetApkAssets(assetManager.mObject, apkAssets,
false /*invalidateCaches*/);
return assetManager;
}
通过getSystem().getApkAssets()获取到系统的asset之后,和应用自身的ApkAssets合并,在创建AssetManager之后将合并后的列表赋给AssetManager对象的成员变量。看下AssetManager的构造函数,代码如下:
private AssetManager(boolean sentinel) {
mObject = nativeCreate();
if (DEBUG_REFS) {
mNumRefs = 0;
incRefsLocked(hashCode());
}
}
nativeCreate是一个native方法,这里先跳过。通过build方法我们得到一个持有系统asset和应用asset的AssetManager对象,接下来我们往上回溯。 我们回到调用createAssetManager的地方createResourcesImpl方法中,新创建返回的AssetManager对象和DisplayMetrics,以及Configuration对象一起作为创建ResourcesImpl的参数。createResourcesImpl得到一个创建的ResourcesImpl对象。在getOrCreateResources方法中,将创建的ResourcesImpl对象作为参数传入到getOrCreateResourcesLocked方法中,代码如下:
private @NonNull Resources getOrCreateResourcesLocked(@NonNull ClassLoader classLoader,
@NonNull ResourcesImpl impl, @NonNull CompatibilityInfo compatInfo) {
// Find an existing Resources that has this ResourcesImpl set.
final int refCount = mResourceReferences.size();
for (int i = 0; i < refCount; i++) {
WeakReference weakResourceRef = mResourceReferences.get(i);
Resources resources = weakResourceRef.get();
if (resources != null &&
Objects.equals(resources.getClassLoader(), classLoader) &&
resources.getImpl() == impl) {
return resources;
}
}
// Create a new Resources reference and use the existing ResourcesImpl object.
Resources resources = compatInfo.needsCompatResources() ? new CompatResources(classLoader)
: new Resources(classLoader);
resources.setImpl(impl);
mResourceReferences.add(new WeakReference<>(resources));
return resources;
}
通过Resources的构造函数创建Resources对象后,将前面创建的ResourcesImpl对象赋值给它的成员变量。随后getOrCreateResources将创建好的Resources对象返回,到此LoadedApk中的getResources就得到了Resources对象。
了解了资源加载的流程后,结合要做资源更新的需求,我们可以整理下资源替换的步骤。
反射拿到ActivityThread对象持有的LoadedApk对象
遍历容器中的LoadedApk对象,反射替换mResDir属性为补丁包的物理路径。
创建新的AssetManager,并根据补丁路径反射调用addAssetPath将补丁加载新的AssetManager对象中。
反射获得ResourcesManager 持有的Resource容器对象
-
遍历容器找那个的Resources对象,替换属性为新的AssetManager,并且根据原属性重新更新Resource对象的配置。
为什么要这么做,完全想不出来,这个流程来自Android 热修复方案Tinker(四) 资源补丁加载。作者是真的厉害,tinker团队也是。
而且此时resource对象还只能获取到asset资源,其他的资源并没有知道加载地方。真是道阻且长!
初步校验补丁
热更新要解决的问题核心就两个,一个是代码加载。一个是资源加载。在调用tryLoadPatchFilesInternal方法检查完patch文件之后,调用loadTinkerJars完成了代码加载,调用loadTinkerResources进行资源加载。前面我们分析了tryLoadPatchFilesInternal这个方法中检查了patch文件,然后进行了代码的热更,下面我们看资源更新的部分。在tryLoadPatchFilesInternal中调用了checkComplete这个方法检查能否进行资源加载。下面我们看下这个方法检查的内容,代码如下:
public static boolean checkComplete(Context context, String directory, ShareSecurityCheck securityCheck, Intent intentResult) {
// RESOURCE_META_FILE文件名是assets/res_meta.txt
String meta = securityCheck.getMetaContentMap().get(RESOURCE_META_FILE);
//not found resource
// 如果没有资源更新,就不会存在assets/res_meta.txt,这个方法直接返回。
if (meta == null) {
return true;
}
//only parse first line for faster
ShareResPatchInfo.parseResPatchInfoFirstLine(meta, resPatchInfo);
if (resPatchInfo.resArscMd5 == null) {
return true;
}
if (!ShareResPatchInfo.checkResPatchInfo(resPatchInfo)) {
intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_PACKAGE_PATCH_CHECK, ShareConstants.ERROR_PACKAGE_CHECK_RESOURCE_META_CORRUPTED);
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_PACKAGE_CHECK_FAIL);
return false;
}
String resourcePath = directory + "/" + RESOURCE_PATH + "/";
File resourceDir = new File(resourcePath);
if (!resourceDir.exists() || !resourceDir.isDirectory()) {
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_RESOURCE_DIRECTORY_NOT_EXIST);
return false;
}
File resourceFile = new File(resourcePath + RESOURCE_FILE);
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;
}
这个方法前面检查了patch,比如是否存在assets/res_meta.txt文件,md5是否匹配,创建目录是否成功等。检查完有个重要的方法isResourceCanPatch,看函数名是判断能否加载资源。这个函数大有玄机,我们仔细看下,代码如下:
public static void isResourceCanPatch(Context context) throws Throwable {
// - Replace mResDir to point to the external resource file instead of the .apk. This is
// used as the asset path for new Resources objects.
// - Set Application#mLoadedApk to the found LoadedApk instance
// Find the ActivityThread instance for the current thread
// 获取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.
Class> loadedApkClass;
try {
// 获取LoadedApk对象
loadedApkClass = Class.forName("android.app.LoadedApk");
} catch (ClassNotFoundException e) {
loadedApkClass = Class.forName("android.app.ActivityThread$PackageInfo");
}
// 根据上文环境拿到ActivityThread和LoadedApk中修复资源相关的三个属性.并且将mResDir,mPackages和 mResourcePackages三个属性设置可访问,并存起来供加载补丁时使用.
resDir = findField(loadedApkClass, "mResDir");
packagesFiled = findField(activityThread, "mPackages");
if (Build.VERSION.SDK_INT < 27) {
resourcePackagesFiled = findField(activityThread, "mResourcePackages");
}
// Create a new AssetManager instance and point it to the resources
final AssetManager assets = context.getAssets();
// 拿到关键方法
addAssetPathMethod = findMethod(assets, "addAssetPath", String.class);
// Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm
// in L, so we do it unconditionally.
try {
stringBlocksField = findField(assets, "mStringBlocks");
ensureStringBlocksMethod = findMethod(assets, "ensureStringBlocks");
} catch (Throwable ignored) {
// Ignored.
}
// Use class fetched from instance to avoid some ROMs that use customized AssetManager
// class. (e.g. Baidu OS)
newAssetManager = (AssetManager) findConstructor(assets).newInstance();
// Iterate over all known Resources objects
// 先将调用单例类ResourcesManager的getInstance方法,拿到ResourcesManager对象
if (SDK_INT >= KITKAT) {
//pre-N
// Find the singleton instance of ResourcesManager
final Class> resourcesManagerClass = Class.forName("android.app.ResourcesManager");
final Method mGetInstance = findMethod(resourcesManagerClass, "getInstance");
final Object resourcesManager = mGetInstance.invoke(null);
try {
Field fMActiveResources = findField(resourcesManagerClass, "mActiveResources");
final ArrayMap, WeakReference> activeResources19 =
(ArrayMap, WeakReference>) fMActiveResources.get(resourcesManager);
references = activeResources19.values();
} catch (NoSuchFieldException ignore) {
// N moved the resources to mResourceReferences
final Field mResourceReferences = findField(resourcesManagerClass, "mResourceReferences");
references = (Collection>) mResourceReferences.get(resourcesManager);
}
} else {// <19的情况下Resources对象集合的引用是在ActivityThread对象的mActiveResources对象持有的.反射拿到之后也将所有的Resources引用保存起来.
final Field fMActiveResources = findField(activityThread, "mActiveResources");
final HashMap, WeakReference> activeResources7 =
(HashMap, WeakReference>) fMActiveResources.get(currentActivityThread);
references = activeResources7.values();
}
// check resource
if (references == null) {
throw new IllegalStateException("resource references is null");
}
final Resources resources = context.getResources();
// fix jianGuo pro has private field 'mAssets' with Resource
// try use mResourcesImpl first
if (SDK_INT >= 24) {
try {
// N moved the mAssets inside an mResourcesImpl field
resourcesImplFiled = findField(resources, "mResourcesImpl");
} catch (Throwable ignore) {
// for safety
assetsFiled = findField(resources, "mAssets");
}
} else {
assetsFiled = findField(resources, "mAssets");
}
try {
publicSourceDirField = findField(ApplicationInfo.class, "publicSourceDir");
} catch (NoSuchFieldException ignore) {
// Ignored.
}
}
做完上面一系列hook操作,如果没有异常发生,就可以认为当前系统是可以做西苑的更新的,然后将更新资源要用到的filed和method保存起来方便后续补丁的使用。
加载资源补丁
接下来我们分析资源的加载过程,loadTinkerResources的代码如下:
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()) {
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匹配等校验完,会调用monkeyPatchExistingResources真正开始加载资源。我们看下这个放啊,代码如下:
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> entry
: ((Map>) value).entrySet()) {
final Object loadedApk = entry.getValue().get();
if (loadedApk == null) {
continue;
}
final String resDirPath = (String) resDir.get(loadedApk);
if (appInfo.sourceDir.equals(resDirPath)) {
// 遍历容器中的每一个LoadedApk对象 并将mResDir属性的值替换成资源补丁包的路径.
resDir.set(loadedApk, externalResourceFile);
}
}
}
// Create a new AssetManager instance and point it to the resources installed under
if (((Integer) addAssetPathMethod.invoke(newAssetManager, externalResourceFile)) == 0) {
throw new IllegalStateException("Could not create new AssetManager");
}
// Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm
// in L, so we do it unconditionally.
if (stringBlocksField != null && ensureStringBlocksMethod != null) {
stringBlocksField.set(newAssetManager, null);
ensureStringBlocksMethod.invoke(newAssetManager);
}
for (WeakReference wr : references) {
final Resources resources = wr.get();
if (resources == null) {
continue;
}
// Set the AssetManager of the Resources instance to our brand new one
try {
//pre-N
// 最后遍历Resources容器, 将每个Resources对象中引用的AssetManager替换成加载过补丁资源的 新的AssetManager对象.
assetsFiled.set(resources, newAssetManager);
} catch (Throwable ignore) {
// N
final Object resourceImpl = resourcesImplFiled.get(resources);
// for Huawei HwResourcesImpl
final Field implAssets = findField(resourceImpl, "mAssets");
implAssets.set(resourceImpl, newAssetManager);
}
clearPreloadTypedArrayIssue(resources);
resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics());
}
// Handle issues caused by WebView on Android N.
// Issue: On Android N, if an activity contains a webview, when screen rotates
// our resource patch may lost effects.
// for 5.x/6.x, we found Couldn't expand RemoteView for StatusBarNotification Exception
if (Build.VERSION.SDK_INT >= 24) {
try {
if (publicSourceDirField != null) {
publicSourceDirField.set(context.getApplicationInfo(), externalResourceFile);
}
} catch (Throwable ignore) {
// Ignored.
}
}
if (!checkResUpdate(context)) {
throw new TinkerRuntimeException(ShareConstants.CHECK_RES_INSTALL_FAIL);
}
}
进行一堆眼花缭乱的操作之后将原来的AssetManager对象替换,替换之后再反射调用updateConfiguration方法,将Resource对象的配置信息更新到最新状态。最后调用checkResUpdate方法检查resource是否更新成功,通过在资源文件里面预埋一个测试文件。如果加载补丁之后能找到测试的资源文件,就说明资源更新成功,否则就认为失败。
因为资源加载流程不太熟悉,所以看资源的热更加载也很费劲,难怪说热更需要掌握较多的系统知识。暂时过了下大概的流程,后续还需要优化深入下。
参考链接
感谢微信的开源,让tinker成为了小厂的基础设置,让很多应用开发者摆脱了热更的诸多麻烦。
感谢先行者的详细分析和无私分享。功力尚浅,请不吝指教。
Android 热修复方案Tinker(四) 资源补丁加载
Android 获取资源的过程分析