/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.fd.runtime;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.os.Build;
import android.util.ArrayMap;
import android.util.Log;
import android.util.LongSparseArray;
import android.util.SparseArray;
import android.view.ContextThemeWrapper;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static android.os.Build.VERSION.SDK_INT;
import static android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH;
import static android.os.Build.VERSION_CODES.JELLY_BEAN;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2;
import static android.os.Build.VERSION_CODES.KITKAT;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static android.os.Build.VERSION_CODES.M;
import static com.android.tools.fd.runtime.BootstrapApplication.LOG_TAG;
/**
* A utility class which uses reflection hacks to replace the application instance and
* the resource data for the current app.
* This is based on the reflection parts of
* com.google.devtools.build.android.incrementaldeployment.StubApplication,
* plus changes to compile on JDK 6.
* <p>
* It now also has a lot of extra reflection machinery to do live resource swapping
* in a running app (e.g. swiping through data structures, updating resource managers,
* flushing cached theme entries, etc.)
* <p>
* The original is
* https://github.com/google/bazel/blob/master/src/tools/android/java/com/google/devtools/build/android/incrementaldeployment/StubApplication.java
* (May 11 revision, ca96e11)
* <p>
* (The code to handle resource loading etc is different; see FileManager.)
* Furthermore, the resource patching was hacked on some more such that it can
* handle live (activity-restart) changes, which allows us to for example patch
* the theme and have existing activities have their themes updated!
* <p>
* Original comment for the StubApplication, which contained the reflection methods:
* <p>
* A stub application that patches the class loader, then replaces itself with the real application
* by applying a liberal amount of reflection on Android internals.
* <p>
* <p>This is, of course, terribly error-prone. Most of this code was tested with API versions
* 8, 10, 14, 15, 16, 17, 18, 19 and 21 on the Android emulator, a Nexus 5 running Lollipop LRX22C
* and a Samsung GT-I5800 running Froyo XWJPE. The exception is {@code monkeyPatchAssetManagers},
* which only works on Kitkat and Lollipop.
* <p>
* <p>Note that due to a bug in Dalvik, this only works on Kitkat if ART is the Java runtime.
* <p>
* <p>Unfortunately, if this does not work, we don't have a fallback mechanism: as soon as we
* build the APK with this class as the Application, we are committed to going through with it.
* <p>
* <p>This class should use as few other classes as possible before the class loader is patched
* because any class loaded before it cannot be incrementally deployed.
*
*
* monkeyPatchApplication( Hook BootstrapApplication ):
* - 1. Hook 掉 ActivityThread 内的所有 BootstrapApplication 为 RealApplication
* - 2. Hook 掉 ActivityThread 内的所有 LoadedApk 内部的:
* - - 2.1 BootstrapApplication 为 RealApplication
* - - 2.2 mResDir 为 externalResourceFile
*
* monkeyPatchExistingResources( 加载补丁资源,并 Hook 进 App 内 ):
* - 1. 反射调用 AssetManager.addAssetPath 方法加载 补丁资源
* - 2. Hook Resource or ResourcesImpl 中的 mAssets,Hook 为 补丁资源
* - 3. Hook Resource or ResourcesImpl 内 Theme or ThemeImpl 中的 mAssets,Hook 为 补丁资源
* - 4. Hook Activity( ContextThemeWrapper )的 initializeTheme 方法去初始化 Theme
* - 5. 如果 < 7.0, 先 Hook AssetManager 的 createTheme 方法去创建一个 补丁 Theme
* - 然后 Hook Activity 的 Theme 的 mTheme Field 为 补丁 Theme
* - 6. 调用 pruneResourceCaches(@NonNull Object resources) 方法去删除 资源缓存
*
* pruneResourceCache( 由于 hook 进来了 newAssetManager,所以需要把原来运行 Activity 的资源缓存清空 ):
* - 1. 删除 Resource 内部的 TypedArrayPool 的资源缓存
* - 2. 删除 Resource 图片、动画、颜色等资源缓存
* - 3. 删除 ResourceImpl 图片、动画、颜色等资源缓存
*/
public class MonkeyPatcher {
/**
* 1. Hook 掉 ActivityThread 内的所有 BootstrapApplication 为 RealApplication
* 2. Hook 掉 ActivityThread 内的所有 LoadedApk 内部的:
* BootstrapApplication 为 RealApplication
* mResDir 为 externalResourceFile
*
* @param context context
* @param bootstrap BootstrapApplication
* @param realApplication realApplication
* @param externalResourceFile 外部资源 path
*/
@SuppressWarnings("unchecked") // Lots of conversions with generic types
public static void monkeyPatchApplication(@Nullable Context context,
@Nullable Application bootstrap,
@Nullable Application realApplication,
@Nullable String externalResourceFile) {
/*
The code seems to perform this:
Application realApplication = the newly instantiated (in attachBaseContext) user app
currentActivityThread = ActivityThread.currentActivityThread;
Application initialApplication = currentActivityThread.mInitialApplication;
if (initialApplication == BootstrapApplication.this) {
currentActivityThread.mInitialApplication = realApplication;
// Replace all instance of the stub application in ActivityThread#mAllApplications with the
// real one
List<Application> allApplications = currentActivityThread.mAllApplications;
for (int i = 0; i < allApplications.size(); i++) {
if (allApplications.get(i) == BootstrapApplication.this) {
allApplications.set(i, realApplication);
}
}
// Enumerate all LoadedApk (or PackageInfo) fields in ActivityThread#mPackages and
// ActivityThread#mResourcePackages and do two things:
// - Replace the Application instance in its mApplication field with the real one
// - 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
ArrayMap<String, WeakReference<LoadedApk>> map1 = currentActivityThread.mPackages;
for (Map.Entry<String, WeakReference<?>> entry : map1.entrySet()) {
Object loadedApk = entry.getValue().get();
if (loadedApk == null) {
continue;
}
if (loadedApk.mApplication == BootstrapApplication.this) {
loadedApk.mApplication = realApplication;
if (externalResourceFile != null) {
loadedApk.mResDir = externalResourceFile;
}
realApplication.mLoadedApk = loadedApk;
}
}
// Exactly the same as above, except done for mResourcePackages instead of mPackages
ArrayMap<String, WeakReference<LoadedApk>> map2 = currentActivityThread.mResourcePackages;
for (Map.Entry<String, WeakReference<?>> entry : map2.entrySet()) {
Object loadedApk = entry.getValue().get();
if (loadedApk == null) {
continue;
}
if (loadedApk.mApplication == BootstrapApplication.this) {
loadedApk.mApplication = realApplication;
if (externalResourceFile != null) {
loadedApk.mResDir = externalResourceFile;
}
realApplication.mLoadedApk = loadedApk;
}
}
*/
// BootstrapApplication is created by reflection in Application#handleBindApplication() ->
// LoadedApk#makeApplication(), and its return value is used to set the Application field in all
// sorts of Android internals.
//
// Fortunately, Application#onCreate() is called quite soon after, so what we do is monkey
// patch in the real Application instance in BootstrapApplication#onCreate().
//
// A few places directly use the created Application instance (as opposed to the fields it is
// eventually stored in). Fortunately, it's easy to forward those to the actual real
// Application class.
try {
/**
* Step 1
* 各种反射寻找该进程的 ActivityThread
*/
// Find the ActivityThread instance for the current thread
Class<?> activityThread = Class.forName("android.app.ActivityThread");
Object currentActivityThread = getActivityThread(context, activityThread);
/**
* Step 2
*
* Hook ActivityThread 内的 BootStrapApplication 数据为 RealApplication
*
* 1.通过 ActivityThread 去 Hook 替换内部的 mInitialApplication Field
* 如果实例化的是 BootStrapApplication 就替换上 RealApplication( 项目真正的 Application )
*
* 2.通过 ActivityThread 去 Hook 修改内部的 mAllApplications Field
* 如果有 BootStrapApplication 就替换上 RealApplication( 项目真正的 Application )
*/
// Find the mInitialApplication field of the ActivityThread to the real application
Field mInitialApplication = activityThread.getDeclaredField("mInitialApplication");
mInitialApplication.setAccessible(true);
Application initialApplication = (Application) mInitialApplication.get(
currentActivityThread);
if (realApplication != null && initialApplication == bootstrap) {
mInitialApplication.set(currentActivityThread, realApplication);
}
// Replace all instance of the stub application in ActivityThread#mAllApplications with the
// real one
if (realApplication != null) {
Field mAllApplications = activityThread.getDeclaredField("mAllApplications");
mAllApplications.setAccessible(true);
List<Application> allApplications = (List<Application>) mAllApplications
.get(currentActivityThread);
for (int i = 0; i < allApplications.size(); i++) {
if (allApplications.get(i) == bootstrap) {
allApplications.set(i, realApplication);
}
}
}
/**
* Step 3
*
* 再次 Hook ActivityThread 内的 BootStrapApplication 数据为 RealApplication
* 并且把资源 dir 和 RealApplication 中的 LoadedApk 也替换了
*
* 对 ActivityThread 的 ArrayMap<String, WeakReference<LoadedApk>> mPackages 和
* ArrayMap<String, WeakReference<LoadedApk>> mResourcePackages 进行 hook。
* 遍历两个 map,如果里面的 LoadedApk 的 mApplication == BootstrapApplication。
* 1. 替换 mApplication 为 自定义 Application;
* 2. 替换 LoadedApk 的 mResDir 为 externalResourceFile;
* 3. 用修改后的 LoadedApk hook 掉 项目 Application 中的 mLoadedApk 。
*/
// Figure out how loaded APKs are stored.
// API version 8 has PackageInfo, 10 has LoadedApk. 9, I don't know.
Class<?> loadedApkClass;
try {
loadedApkClass = Class.forName("android.app.LoadedApk");
} catch (ClassNotFoundException e) {
loadedApkClass = Class.forName("android.app.ActivityThread$PackageInfo");
}
Field mApplication = loadedApkClass.getDeclaredField("mApplication");
mApplication.setAccessible(true);
Field mResDir = loadedApkClass.getDeclaredField("mResDir");
mResDir.setAccessible(true);
// 10 doesn't have this field, 14 does. Fortunately, there are not many Honeycomb devices
// floating around.
Field mLoadedApk = null;
try {
mLoadedApk = Application.class.getDeclaredField("mLoadedApk");
} catch (NoSuchFieldException e) {
// According to testing, it's okay to ignore this.
}
// Enumerate all LoadedApk (or PackageInfo) fields in ActivityThread#mPackages and
// ActivityThread#mResourcePackages and do two things:
// - Replace the Application instance in its mApplication field with the real one
// - 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
for (String fieldName : new String[] { "mPackages", "mResourcePackages" }) {
Field field = activityThread.getDeclaredField(fieldName);
field.setAccessible(true);
Object value = field.get(currentActivityThread);
for (Map.Entry<String, WeakReference<?>> entry :
((Map<String, WeakReference<?>>) value).entrySet()) {
Object loadedApk = entry.getValue().get();
if (loadedApk == null) {
continue;
}
if (mApplication.get(loadedApk) == bootstrap) {
if (realApplication != null) {
mApplication.set(loadedApk, realApplication);
}
if (externalResourceFile != null) {
mResDir.set(loadedApk, externalResourceFile);
}
if (realApplication != null && mLoadedApk != null) {
mLoadedApk.set(realApplication, loadedApk);
}
}
}
}
} catch (Throwable e) {
throw new IllegalStateException(e);
}
}
/**
* 反射获取该进程实例化的 ActivityThread
* 1. 先从 ActivityThread 内找实例对象
* 2. 再反射到 Application -> LoadedApk ,在 LoadedApk 内寻找实例对象
*
* @param context context
* @param activityThread ActivityThread Class
* @return ActivityThread 实例
*/
@Nullable
public static Object getActivityThread(@Nullable Context context,
@Nullable Class<?> activityThread) {
try {
/**
* 通过反射 ActivityThread 的 currentActivityThread 方法
* 拿到 ActivityThread 实例化
*/
if (activityThread == null) {
activityThread = Class.forName("android.app.ActivityThread");
}
Method m = activityThread.getMethod("currentActivityThread");
m.setAccessible(true);
Object currentActivityThread = m.invoke(null);
/**
* 这的 Context 一般都是 Application
* 所以,反射 Application 的 mLoadedApk Field( LoadedApk )
* mLoadedApk 属性是一个 LoadedApk 类型
* 再反射 LoadedApk 的 mActivityThread Field( ActivityThread )
* 拿到 返回 该 ActivityThread 对象
*/
if (currentActivityThread == null && context != null) {
// In older versions of Android (prior to frameworks/base 66a017b63461a22842)
// the currentActivityThread was built on thread locals, so we'll need to try
// even harder
Field mLoadedApk = context.getClass().getField("mLoadedApk");
mLoadedApk.setAccessible(true);
Object apk = mLoadedApk.get(context);
Field mActivityThreadField = apk.getClass().getDeclaredField("mActivityThread");
mActivityThreadField.setAccessible(true);
currentActivityThread = mActivityThreadField.get(apk);
}
return currentActivityThread;
} catch (Throwable ignore) {
return null;
}
}
/**
* 1. 反射调用 AssetManager.addAssetPath 方法加载 补丁资源
* 2. Hook Resource or ResourcesImpl 中的 mAssets,Hook 为 补丁资源
* 3. Hook Resource or ResourcesImpl 内 Theme or ThemeImpl 中的 mAssets,Hook 为 补丁资源
* 4. Hook Activity( ContextThemeWrapper )的 initializeTheme 方法去初始化 Theme
* 5. 如果 < 7.0, 先 Hook AssetManager 的 createTheme 方法去创建一个 补丁 Theme
* 然后 Hook Activity 的 Theme 的 mTheme Field 为 补丁 Theme
* 6. 调用 pruneResourceCaches(@NonNull Object resources) 方法去删除 资源缓存
*
* @param context context
* @param externalResourceFile 外部资源 path
* @param activities 运行 activity
*/
public static void monkeyPatchExistingResources(@Nullable Context context,
@Nullable String externalResourceFile,
@Nullable Collection<Activity> activities) {
if (externalResourceFile == null) {
return;
}
/*
(Note: the resource directory is *also* inserted into the loadedApk in
monkeyPatchApplication)
The code seems to perform this:
File externalResourceFile = <path to resources.ap_ or extracted directory>
AssetManager newAssetManager = new AssetManager();
newAssetManager.addAssetPath(externalResourceFile)
// Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm
// in L, so we do it unconditionally.
newAssetManager.ensureStringBlocks();
// Find the singleton instance of ResourcesManager
ResourcesManager resourcesManager = ResourcesManager.getInstance();
// Iterate over all known Resources objects
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
for (WeakReference<Resources> wr : resourcesManager.mActiveResources.values()) {
Resources resources = wr.get();
// Set the AssetManager of the Resources instance to our brand new one
resources.mAssets = newAssetManager;
resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics());
}
}
// Also, for each context, call getTheme() to get the current theme; null out its
// mTheme field, then invoke initializeTheme() to force it to be recreated (with the
// new asset manager!)
*/
try {
/**
* Step 1
*
* 反射 AssetManager#addAssetPath 方法
* 加载补丁资源的 AssetManager
*/
// Create a new AssetManager instance and point it to the resources installed under
// /sdcard
AssetManager newAssetManager = AssetManager.class.getConstructor().newInstance();
Method mAddAssetPath = AssetManager.class.getDeclaredMethod("addAssetPath",
String.class);
mAddAssetPath.setAccessible(true);
if (((Integer) mAddAssetPath.invoke(newAssetManager, externalResourceFile)) == 0) {
throw new IllegalStateException("Could not create new AssetManager");
}
/**
* Step 2
*
* 反射 Hook 补丁资源 AssetManager 的 ensureStringBlocks Field 为 true
* 下面注释告诉,4.4 需要这么做
*/
// Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm
// in L, so we do it unconditionally.
Method mEnsureStringBlocks = AssetManager.class.getDeclaredMethod("ensureStringBlocks");
mEnsureStringBlocks.setAccessible(true);
mEnsureStringBlocks.invoke(newAssetManager);
if (activities != null) {
for (Activity activity : activities) {
Resources resources = activity.getResources();
/**
* Step 3
*
* 获取每个 Activity
* 然后 Hook 每个 Resource 的 mAssets Field
* 设置为 补丁 AssetManager
*
* 如果没有 mAssets Field 就
* 去找 mResourcesImpl ( ResourcesImpl ) Field
* 然后 Hook ResourcesImpl 的 mAssets Field
* 设置为 补丁 AssetManager
*/
try {
Field mAssets = Resources.class.getDeclaredField("mAssets");
mAssets.setAccessible(true);
mAssets.set(resources, newAssetManager);
} catch (Throwable ignore) {
Field mResourcesImpl = Resources.class.getDeclaredField("mResourcesImpl");
mResourcesImpl.setAccessible(true);
Object resourceImpl = mResourcesImpl.get(resources);
Field implAssets = resourceImpl.getClass().getDeclaredField("mAssets");
implAssets.setAccessible(true);
implAssets.set(resourceImpl, newAssetManager);
}
Resources.Theme theme = activity.getTheme();
try {
/**
* Step 4
*
* 进一步 拿到 Resource 的 Theme
* 然后 Hook Theme 的 mAssets Field
* 设置为 补丁 AssetManager
*
* 如果没有 mAssets Field 就
* 去找 mThemeImpl ( ResourcesImpl.ThemeImpl ) Field
* 然后 Hook ThemeImpl 的 mAssets Field
* 设置为 补丁 AssetManager
*/
try {
Field ma = Resources.Theme.class.getDeclaredField("mAssets");
ma.setAccessible(true);
ma.set(theme, newAssetManager);
} catch (NoSuchFieldException ignore) {
Field themeField = Resources.Theme.class.getDeclaredField("mThemeImpl");
themeField.setAccessible(true);
Object impl = themeField.get(theme);
Field ma = impl.getClass().getDeclaredField("mAssets");
ma.setAccessible(true);
ma.set(impl, newAssetManager);
}
/**
* Step 5
*
* Hook Activity( ContextThemeWrapper )的 mTheme Field 为 null
* Hook Activity( ContextThemeWrapper )的 initializeTheme 方法去初始化 Theme
*/
Field mt = ContextThemeWrapper.class.getDeclaredField("mTheme");
mt.setAccessible(true);
mt.set(activity, null);
Method mtm = ContextThemeWrapper.class.getDeclaredMethod("initializeTheme");
mtm.setAccessible(true);
mtm.invoke(activity);
/**
* Step 6
*
* 如果 < 24
*
* 先 Hook AssetManager 的 createTheme 方法
* 去创建一个 补丁 Theme
*
* 然后 Hook Activity 的 Theme 的 mTheme Field 为 补丁 Theme
*/
if (SDK_INT < 24) { // As of API 24, mTheme is gone (but updates work
// without these changes
Method mCreateTheme = AssetManager.class
.getDeclaredMethod("createTheme");
mCreateTheme.setAccessible(true);
Object internalTheme = mCreateTheme.invoke(newAssetManager);
Field mTheme = Resources.Theme.class.getDeclaredField("mTheme");
mTheme.setAccessible(true);
mTheme.set(theme, internalTheme);
}
} catch (Throwable e) {
Log.e(LOG_TAG, "Failed to update existing theme for activity " + activity,
e);
}
/**
* Step 7
*
* 调用 pruneResourceCaches(@NonNull Object resources) 方法去删除 资源缓存
*/
pruneResourceCaches(resources);
}
}
/**
* Step 8
*
* 1. 如果 > 4.4,反射拿到 ResourcesManager 的 getInstance 方法。获取 ResourcesManager 的单例对象,
* 有两种选择:
* 1.1 反射获取 mActiveResources Field,强转为 ArrayMap<?, WeakReference<Resources>> 类型,赋值给 references
* 1.2 反射获取 mResourceReferences Field,强转为 Collection<WeakReference<Resources>> 类型,赋值给 references
*
* 2. 如果 <= 4.4,通过 getActivityThread 方法去获取进程中的 ActivityThread 实例。 然后反射
* 获取 ActivityThread 的 mActiveResources Field,强转为 HashMap<?, WeakReference<Resources>> 类型,赋值给 references
*
* 3. 将 1 or 2 环境中,保存下来的 references 进行遍历,拿到每一个 WeakReference<Resources>
* 有两种选择:
* 3.1 Hook Resource 的 mAssets Field 的值为 补丁 AssetManager
* 3.2 如果 3.1 失败被 catch 了,反射拿到 Resource mResourcesImpl Field( ResourcesImpl )
* 然后 Hook ResourcesImpl 的 mAssets Field 的值为 补丁 AssetManager
*
*
*/
// Iterate over all known Resources objects
Collection<WeakReference<Resources>> references;
if (SDK_INT >= KITKAT) {
// Find the singleton instance of ResourcesManager
Class<?> resourcesManagerClass = Class.forName("android.app.ResourcesManager");
Method mGetInstance = resourcesManagerClass.getDeclaredMethod("getInstance");
mGetInstance.setAccessible(true);
Object resourcesManager = mGetInstance.invoke(null);
try {
Field fMActiveResources = resourcesManagerClass.getDeclaredField(
"mActiveResources");
fMActiveResources.setAccessible(true);
@SuppressWarnings("unchecked")
ArrayMap<?, WeakReference<Resources>> arrayMap =
(ArrayMap<?, WeakReference<Resources>>) fMActiveResources.get(
resourcesManager);
references = arrayMap.values();
} catch (NoSuchFieldException ignore) {
Field mResourceReferences = resourcesManagerClass.getDeclaredField(
"mResourceReferences");
mResourceReferences.setAccessible(true);
//noinspection unchecked
references = (Collection<WeakReference<Resources>>) mResourceReferences.get(
resourcesManager);
}
} else {
Class<?> activityThread = Class.forName("android.app.ActivityThread");
Field fMActiveResources = activityThread.getDeclaredField("mActiveResources");
fMActiveResources.setAccessible(true);
Object thread = getActivityThread(context, activityThread);
@SuppressWarnings("unchecked")
HashMap<?, WeakReference<Resources>> map =
(HashMap<?, WeakReference<Resources>>) fMActiveResources.get(thread);
references = map.values();
}
for (WeakReference<Resources> wr : references) {
Resources resources = wr.get();
if (resources != null) {
// Set the AssetManager of the Resources instance to our brand new one
try {
Field mAssets = Resources.class.getDeclaredField("mAssets");
mAssets.setAccessible(true);
mAssets.set(resources, newAssetManager);
} catch (Throwable ignore) {
Field mResourcesImpl = Resources.class.getDeclaredField("mResourcesImpl");
mResourcesImpl.setAccessible(true);
Object resourceImpl = mResourcesImpl.get(resources);
Field implAssets = resourceImpl.getClass().getDeclaredField("mAssets");
implAssets.setAccessible(true);
implAssets.set(resourceImpl, newAssetManager);
}
resources.updateConfiguration(resources.getConfiguration(),
resources.getDisplayMetrics());
}
}
} catch (Throwable e) {
throw new IllegalStateException(e);
}
}
/**
* 删除 资源缓存
*
* 1. 删除 Resource 内部的 TypedArrayPool 的资源缓存
* 2. 删除 Resource 图片、动画、颜色等资源缓存
* 3. 删除 ResourceImpl 图片、动画、颜色等资源缓存
*
* @param resources resources
*/
private static void pruneResourceCaches(@NonNull Object resources) {
// Drain TypedArray instances from the typed array pool since these can hold on
// to stale asset data
/**
* Step 1
*
* 如果 >= 5.0
*
* 反射获取 Resource 的 mTypedArrayPool Field
* 去到该 Field 的 Class ( SynchronizedPool )
* 然后再反射获取 SynchronizedPool 的 acquire 方法
*
* 通过不断 反射调用 acquire 释放 TypedArray 数据( 资源 )
*/
if (SDK_INT >= LOLLIPOP) {
try {
Field typedArrayPoolField =
Resources.class.getDeclaredField("mTypedArrayPool");
typedArrayPoolField.setAccessible(true);
Object pool = typedArrayPoolField.get(resources);
Class<?> poolClass = pool.getClass();
Method acquireMethod = poolClass.getDeclaredMethod("acquire");
acquireMethod.setAccessible(true);
while (true) {
Object typedArray = acquireMethod.invoke(pool);
if (typedArray == null) {
break;
}
}
} catch (Throwable ignore) {
}
}
/**
* Step 2
*
* 如果 >= 6.0
*
* 反射获取 Resource 的 mResourcesImpl Field
* 然后通过传进来的 resource 对象去获取这个 Field 的对象 ResourcesImpl
*
* 在 >= 6.0 的版本,需要用 ResourcesImpl 去代替 Resource
* 所以最后将传进来的 Resource 替换为 ResourcesImpl
*
* 所以以下的步骤分为两个分水岭
* >= 6.0 的,resource 的对象类型为 ResourcesImpl
* < 6.0 的,resource 的对象类型为 Resources
*/
if (SDK_INT >= Build.VERSION_CODES.M) {
// Really should only be N; fix this as soon as it has its own API level
try {
Field mResourcesImpl = Resources.class.getDeclaredField("mResourcesImpl");
mResourcesImpl.setAccessible(true);
// For the remainder, use the ResourcesImpl instead, where all the fields
// now live
resources = mResourcesImpl.get(resources);
} catch (Throwable ignore) {
}
}
/**
* Step 3
*
* 由于以上做了 resource 的修改
*
* 由于受到 Step 2 的影响,所以这里的话
*
* >= 4.3
* 再 >= 6.0 ,会取到 ResourcesImpl 的 mAccessLock Field
* 然后,然后获得该锁 ( Object mAccessLock )
*
* 再 < 6.0 ,会取到 Resources 的 mAccessLock Field
* 然后,然后获得该锁 ( Object mAccessLock )
*
* < 4.3 ,直接获取 Resources 的 mTmpValue Field
* 然后,然后获得该锁 ( Object mAccessLock )
*
* 如果都没找到锁,就拿该类( MonkeyPatcher.class )作为锁
*/
// Prune bitmap and color state lists etc caches
Object lock = null;
if (SDK_INT >= JELLY_BEAN_MR2) {
try {
Field field = resources.getClass().getDeclaredField("mAccessLock");
field.setAccessible(true);
lock = field.get(resources);
} catch (Throwable ignore) {
}
} else {
try {
Field field = Resources.class.getDeclaredField("mTmpValue");
field.setAccessible(true);
lock = field.get(resources);
} catch (Throwable ignore) {
}
}
if (lock == null) {
lock = MonkeyPatcher.class;
}
/**
* Step 4
*
* 由于再次受到 Step 2 的影响
*
* 如果 >= 6.0 会删除
* ResourcesImpl 内 mDrawableCache、mColorDrawableCache、mColorStateListCache、
* mAnimatorCache、mStateListAnimatorCache 的资源缓存
*
* 如果 < 6.0 会删除
* ResourcesImpl 内的 mDrawableCache、mColorDrawableCache、mColorStateListCache 资源缓存
* 同时,如果还是 4.3 的话,会额外删除
* Resources 内的 sPreloadedDrawables、sPreloadedColorDrawables、sPreloadedColorStateLists
* 资源缓存
*/
//noinspection SynchronizationOnLocalVariableOrMethodParameter
synchronized (lock) {
// Prune bitmap and color caches
pruneResourceCache(resources, "mDrawableCache");
pruneResourceCache(resources, "mColorDrawableCache");
pruneResourceCache(resources, "mColorStateListCache");
if (SDK_INT >= M) {
pruneResourceCache(resources, "mAnimatorCache");
pruneResourceCache(resources, "mStateListAnimatorCache");
} else if (SDK_INT == KITKAT) {
pruneResourceCache(resources, "sPreloadedDrawables");
pruneResourceCache(resources, "sPreloadedColorDrawables");
pruneResourceCache(resources, "sPreloadedColorStateLists");
}
}
}
/**
* 如果 < 6.0 会删除
* ResourcesImpl 内的 mDrawableCache、mColorDrawableCache、mColorStateListCache 资源缓存
* 同时,如果还是 4.3 的话,会额外删除
* Resources 内的 sPreloadedDrawables、sPreloadedColorDrawables、sPreloadedColorStateLists
* 资源缓存
*
* @param resources ResourcesImpl or Resources
* @param fieldName 以上 field name
* @return 是否清空
*/
private static boolean pruneResourceCache(@NonNull Object resources,
@NonNull String fieldName) {
try {
/**
* Step 1
*
* 从 ResourcesImpl or Resources 拿到对应的 fieldName 的 Field
*
* 当然,如果 ResourcesImpl 获取失败了,会 catch 。
* 然后直接 Resources.class.getDeclaredField(fieldName) 去拿 Field
*/
Class<?> resourcesClass = resources.getClass();
Field cacheField;
try {
cacheField = resourcesClass.getDeclaredField(fieldName);
} catch (NoSuchFieldException ignore) {
cacheField = Resources.class.getDeclaredField(fieldName);
}
cacheField.setAccessible(true);
Object cache = cacheField.get(resources);
/**
* Step 2
*
* 上面拿到的 Field
* 然后判断 Field 的类型:
*
* 1 如果 < 4.1,有两种选择:
* 1.1 如果属于 SparseArray 类型,直接 clear 清空,返回 true
* 1.2 如果 => 4.0 并且 属于 LongSparseArray,也是直接 clear 清空,返回 true
*
* 2 如果 4.1 < x < 6.0,有四种选择:
* 2.1 如果 Field 是 mColorStateListCache,并且属于 LongSparseArray,也是直接 clear 清空
* 2.2 如果 Field 的类型实现了 ArrayMap 的超类( Map ),然后反射调用 Resource clearDrawableCachesLocked 方法
* 2.3 如果 Field 是类型实现了 LongSparseArray 的超类 ( Cloneable ? ),然后反射调用 Resource clearDrawableCachesLocked 方法
* 注: 2.2 与 2.3 的区别在于 clearDrawableCachesLocked 的参数不一样,一个是 ArrayMap,一个是 LongSparseArray
*
* 2.4 如果 Field 是类型实现是 数组类型,并且数组的 class 类型( getComponentType ) 实现了 LongSparseArray 的超类 ( Cloneable ? )
* 然后强转为 LongSparseArray[] 类型,一个一个拿出 LongSparseArray,然后 clear
*
* 3. 如果 >= 6.0 ( 主要针对 Marshmallow: DrawableCache class )
* 反射拿到该 Field 的 onConfigurationChange 的方法并且调用
* 如果有就返回
* 如果没有的话,继续拿到父类,继续反射拿到 onConfigurationChange 的方法并且调用。直到调用过一次 onConfigurationChange 为止
*
* 4. 如果 1-3 内又没一个选择的话,那么就是没有做任何 删除资源缓存操作 or 删除资源缓存失败了
*/
// Find the class which defines the onConfigurationChange method
Class<?> type = cacheField.getType();
if (SDK_INT < JELLY_BEAN) {
if (cache instanceof SparseArray) {
((SparseArray) cache).clear();
return true;
} else if (SDK_INT >= ICE_CREAM_SANDWICH && cache instanceof LongSparseArray) {
// LongSparseArray has API level 16 but was private (and available inside
// the framework) in 15 and is used for this cache.
//noinspection AndroidLintNewApi
((LongSparseArray) cache).clear();
return true;
}
} else if (SDK_INT < M) {
// JellyBean, KitKat, Lollipop
if ("mColorStateListCache".equals(fieldName)) {
// For some reason framework doesn't call clearDrawableCachesLocked on
// this field
if (cache instanceof LongSparseArray) {
//noinspection AndroidLintNewApi
((LongSparseArray) cache).clear();
}
} else if (type.isAssignableFrom(ArrayMap.class)) {
Method clearArrayMap = Resources.class.getDeclaredMethod(
"clearDrawableCachesLocked", ArrayMap.class, Integer.TYPE);
clearArrayMap.setAccessible(true);
clearArrayMap.invoke(resources, cache, -1);
return true;
} else if (type.isAssignableFrom(LongSparseArray.class)) {
try {
Method clearSparseMap = Resources.class.getDeclaredMethod(
"clearDrawableCachesLocked", LongSparseArray.class, Integer.TYPE);
clearSparseMap.setAccessible(true);
clearSparseMap.invoke(resources, cache, -1);
return true;
} catch (NoSuchMethodException e) {
if (cache instanceof LongSparseArray) {
//noinspection AndroidLintNewApi
((LongSparseArray) cache).clear();
return true;
}
}
} else if (type.isArray() &&
type.getComponentType().isAssignableFrom(LongSparseArray.class)) {
LongSparseArray[] arrays = (LongSparseArray[]) cache;
for (LongSparseArray array : arrays) {
if (array != null) {
//noinspection AndroidLintNewApi
array.clear();
}
}
return true;
}
} else {
// Marshmallow: DrawableCache class
while (type != null) {
try {
Method configChangeMethod = type.getDeclaredMethod(
"onConfigurationChange", Integer.TYPE);
configChangeMethod.setAccessible(true);
configChangeMethod.invoke(cache, -1);
return true;
} catch (Throwable ignore) {
}
type = type.getSuperclass();
}
}
} catch (Throwable ignore) {
// Not logging these; while there is some checking of SDK_INT here to avoid
// doing a lot of unnecessary field lookups, it's not entirely accurate and
// errs on the side of caution (since different devices may have picked up
// different snapshots of the framework); therefore, it's normal for this
// to attempt to look up a field for a cache that isn't there; only if it's
// really there will it continue to flush that particular cache.
}
return false;
}
}