1. LoadedPlugin
由于插件是不安装的,为了宿主可以与插件正常工作,需要宿主可以加载插件的类,可以访问插件的静态资源和本地库.
LoadedPlugin 代表着插件 APK,一个 LoadedPlugin 对应一个插件,LoadedPlugin 不但保存了插件 APK 一切信息还负责打通宿主与插件之间的屏障,使宿主可以加载插件的类,可以加载插件的资源和本地库.
1.1 构造方法
// LoadedPlugin.java
protected Resources mResources;
public LoadedPlugin(PluginManager pluginManager, Context context, File apk) throws Exception {
// ...
this.mResources = createResources(context, getPackageName(), apk);
// ...
}
LoadedPlugin 的成员变量 mResources 是访问插件静态资源的 Resources .构造方法内会调用 createResources() 初始化 LoadedPlugin.mResources.
这里 Context 是宿主 Application Context ,是初始化 VirtualApk 时传入的,请记住这对后面阅读有帮助.
1.2 LoadedPlugin.createResources()
// LoadedPlugin.java
protected Resources createResources(Context context, String packageName, File apk) throws Exception {
if (Constants.COMBINE_RESOURCES) {
return ResourcesManager.createResources(context, packageName, apk);
} else {
// 宿主 Resource
Resources hostResources = context.getResources();
// 构建只加载插件 APK 的 AssetManager
AssetManager assetManager = createAssetManager(context, apk);
// 构建新的 Resources ,只加载插件 apk 资源
return new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());
}
}
// Constants.java
public class Constants {
// ...
public static final boolean COMBINE_RESOURCES = true;
// ...
}
createResources() 目的是构建一个新的 Resources ,这个 Resources 可以同时访问宿主和插件的资源或者只可以访问插件资源,由常量 COMBINE_RESOURCES 控制,默认为 true.
为了方便先看只访问插件资源的情况,很简单,只需要构建一个新的只加载插件 APK AssetManager,然后用它构建新的 Resources 即可.
1.3 LoadedPlugin.createAssetManager()
// LoadedPlugin.java
protected AssetManager createAssetManager(Context context, File apk) throws Exception {
AssetManager am = AssetManager.class.newInstance();
Reflector.with(am).method("addAssetPath", String.class).call(apk.getAbsolutePath());
return am;
}
- createAssetManager() 首先通过 AssetManager.class.newInstance() 构建一个新的 AssetManager 实例.
- 然后通过反射调用 AssetManager.addAssetPath() 把插件 apk 路径添加到 AssetManager 的加载路径.
现在回头看当 COMBINE_RESOURCES 为 true 时构建 Resources 需要调用 ResourcesManager.createResources().
2. VirtualAPK 的 ResourcesManager
本文中会出现两个 ResourcesManager ,一个是 VirtualApk 的 ResourcesManager 一个是系统的 ResourcesManager,下面这个是 VirtualApk 的 ResourcesManager.
VirtualApk.ResourcesManager 是创建和管理插件 Resources 的类,是 LoadedPlugin 打破墙壁让宿主访问插件资源的关键角色.
2.1 ResourcesManager.ResourcesManager.createResources()
// ResourcesManager.java
public static synchronized Resources createResources(Context hostContext, String packageName, File apk) throws Exception {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// API >= 24
return createResourcesForN(hostContext, packageName, apk);
}
// 构建新的 Resources
Resources resources = ResourcesManager.createResourcesSimple(hostContext, apk.getAbsolutePath());
// 用新的 Resources Hook 宿主 Resources ( API < 24 )
ResourcesManager.hookResources(hostContext, resources);
return resources;
}
实现方式以 Android7.0 (API - 24) 为界限分为两种:
7.0 以前需要适配各种厂商的方式构建 Resources ,然后通过 Hook 的方式替代宿主进程正在使用的所有 Resources.
7.0 之后通过利用系统 API 优雅地帮助我们构建新的 Resources,不需要适配厂商也不需要 Hook.
先从低版本实现说起,从 ResourcesManager.createResourcesSimple() 开始.
API 1 - 23 (Android1.0 - Android7.0)
2.2 ResourcesManager.createResourcesSimple()
// ResourcesManager.java
private static Resources createResourcesSimple(Context hostContext, String apk) throws Exception {
// 宿主 Resources
Resources hostResources = hostContext.getResources();
Resources newResources = null;
AssetManager assetManager;
Reflector reflector = Reflector.on(AssetManager.class).method("addAssetPath", String.class);
// 1.
// 根据API版本不同构建新的 AssetManager 或者直接获取宿主的 AssetManager
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
// API 1 - 20
assetManager = AssetManager.class.newInstance();
reflector.bind(assetManager);
final int cookie1 = reflector.call(hostContext.getApplicationInfo().sourceDir);
if (cookie1 == 0) {
throw new RuntimeException("createResources failed, can't addAssetPath for " + hostContext.getApplicationInfo().sourceDir);
}
} else {
// API 21 - 23
assetManager = hostResources.getAssets();
reflector.bind(assetManager);
}
// 调用 AssetManager.addAssetPath() 添加 apk 路径
final int cookie2 = reflector.call(apk);
if (cookie2 == 0) {
// AssetManager.addAssetPath() 返回 0 表示失败
throw new RuntimeException("createResources failed, can't addAssetPath for " + apk);
}
// 获取所有插件并调用 AssetManager.addAssetPath() 添加其他插件apk路径
List pluginList = PluginManager.getInstance(hostContext).getAllLoadedPlugins();
for (LoadedPlugin plugin : pluginList) {
final int cookie3 = reflector.call(plugin.getLocation());
if (cookie3 == 0) {
throw new RuntimeException("createResources failed, can't addAssetPath for " + plugin.getLocation());
}
}
// 2.
// 针对不同厂商用相应方法构建新的 Resources
if (isMiUi(hostResources)) {
// 小米
newResources = MiUiResourcesCompat.createResources(hostResources, assetManager);
} else if (isVivo(hostResources)) {
// Vivo
newResources = VivoResourcesCompat.createResources(hostContext, hostResources, assetManager);
} else if (isNubia(hostResources)) {
// 努比亚
newResources = NubiaResourcesCompat.createResources(hostResources, assetManager);
} else if (isNotRawResources(hostResources)) {
// 非原生
newResources = AdaptationResourcesCompat.createResources(hostResources, assetManager);
} else {
// is raw android resources
// 原生
newResources = new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());
}
// 3.
// lastly, sync all LoadedPlugin to newResources
// 使用新的 Resources 更新所有插件的 Resources
for (LoadedPlugin plugin : pluginList) {
plugin.updateResources(newResources);
}
return newResources;
}
// AssetManager.java (Android 5.0)
/**
* Add an additional set of assets to the asset manager. This can be
* either a directory or ZIP file. Not for use by applications. Returns
* the cookie of the added asset, or 0 on failure.
* {@hide}
*/
public final int addAssetPath(String path) {
synchronized (this) {
int res = addAssetPathNative(path);
makeStringBlocks(mStringBlocks);
return res;
}
}
createResourcesSimple() 看似很长,主要分三步:
-
根据系统版本的不同构建新的或者复用宿主 AssetManager ,然后通过反射调用 AssetManager.addAssetPath() 添加插件 apk 路径,最后遍历把所有 LoadedPlugins 的插件 APK 路径都调用 AssetManager.addAssetPath() 添加进去.
- API 1-20: 创建新的 AssetManager 实例.
- API 21-23: 获取宿主的 AssetManager 实例.
根据不同厂商创建新的 Resources 实例.
更新所有 LoadedPlugin 的 Resources.
每一步都在代码上标了注释.其中 addAssetPath() 返回值可以判断是否添加成功.
低版本构建 Resources 的时候不单只加载 LoadedPlugin 对应的插件 APK 资源,还添加其他插件 APK 的路径,最后还要更新所有插件的 Resources,所以所有 LoadedPlugin.mResources 指向的都是同一个 Resources,且这个 Resources 可以访问宿主和所有插件 APK 的资源.
下面稍微看下适配厂家构建 Resources 的代码.
// ResourcesManager.java
private static final class MiUiResourcesCompat {
private static Resources createResources(Resources hostResources, AssetManager assetManager) throws Exception {
Reflector reflector = Reflector.on("android.content.res.MiuiResources");
Resources newResources = reflector.constructor(AssetManager.class, DisplayMetrics.class, Configuration.class)
.newInstance(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());
return newResources;
}
}
private static final class VivoResourcesCompat {
private static Resources createResources(Context hostContext, Resources hostResources, AssetManager assetManager) throws Exception {
Reflector reflector = Reflector.on("android.content.res.VivoResources");
Resources newResources = reflector.constructor(AssetManager.class, DisplayMetrics.class, Configuration.class)
.newInstance(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());
reflector.method("init", String.class).callByCaller(newResources, hostContext.getPackageName());
reflector.field("mThemeValues");
reflector.set(newResources, reflector.get(hostResources));
return newResources;
}
}
private static final class NubiaResourcesCompat {
private static Resources createResources(Resources hostResources, AssetManager assetManager) throws Exception {
Reflector reflector = Reflector.on("android.content.res.NubiaResources");
Resources newResources = reflector.constructor(AssetManager.class, DisplayMetrics.class, Configuration.class)
.newInstance(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());
return newResources;
}
}
private static final class AdaptationResourcesCompat {
private static Resources createResources(Resources hostResources, AssetManager assetManager) throws Exception {
Resources newResources;
try {
Reflector reflector = Reflector.with(hostResources);
newResources = reflector.constructor(AssetManager.class, DisplayMetrics.class, Configuration.class)
.newInstance(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());
} catch (Exception e) {
newResources = new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());
}
return newResources;
}
}
由于厂家定制 ROM 的原因会有厂家定制的 Resources ,当然原生的 Resources 是保留的,所以针对不适配的厂家就会创建原生的 Resources .通过反射调用构造方法创建实例.
ResourcesManager.createResourcesSimple() 到这里结束了,接下来看 ResourcesManager.hookResources(hostContext, resources)
2.3 ResourcesManager.hookResources()
public static void hookResources(Context base, Resources resources) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return;
}
// API 1 - 23
try {
// 1.反射替代 ContextImpl.mResources
Reflector reflector = Reflector.with(base);
reflector.field("mResources").set(resources);
// 2.反射替代 ContextImpl.mPackageInfo.mResources
Object loadedApk = reflector.field("mPackageInfo").get();
Reflector.with(loadedApk).field("mResources").set(resources);
// 获取宿主 ActivityThread
Object activityThread = ActivityThread.currentActivityThread();
Object resManager;
// 3.根据版本不同获取 ResourcesManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
// API 19 - 23
resManager = android.app.ResourcesManager.getInstance();
} else {
// API 1 < 18
resManager = Reflector.with(activityThread).field("mResourcesManager").get();
}
// 获取保存 Resources 弱引用的 ResourcesManager.mActiveResources
// ArrayMap>
Map
首先用反射把新的 Resources 替换宿主 ContextImpl.mResources 指向的 Resources 对象.
用新的 Resources 替换宿主 ContextImpl.mPackageInfo.mResources 指向的对象.
-
根据系统版本的不同获取系统的 ResourcesManager.
API 1 - 18: 反射获取 ActivityThread.mResourcesManager.
API 19 - 23: 直接创建系统 ResourcesManager 实例.
反射把新的 Resources 添加到 ResourcesManager.mActiveResources.
Android 7.0 以前的 ResourcesManager 会有一个 Map 保存 Resources 的弱引用和他对应的 ResourcesKey.
// ResourcesManager.java (Android 4.4.4)
final ArrayMap > mActiveResources = new ArrayMap >();
低版本创建 Resources 并替换的实现到这里结束了,下面从 createResourcesForN() 开始看高版本的实现.
API 24 - lastest (Android7.0 - lastest)
2.4 ResourcesManager.createResourcesForN()
首先看方法注释
/**
* Use System Apis to update all existing resources.
*
* 1. Update ApplicationInfo.splitSourceDirs and LoadedApk.mSplitResDirs
*
* 2. Replace all keys of ResourcesManager.mResourceImpls to new ResourcesKey
*
* 3. Use ResourcesManager.appendLibAssetForMainAssetPath(appInfo.publicSourceDir, "${packageName}.vastub") to update all existing resources.
*
*
* see android.webkit.WebViewDelegate.addWebViewAssetPath(Context)
*/
使用系统 API 更新所有 Resources (宿主+插件)
更新 ApplicationInfo.splitSourceDirs 和 LoadedApk.mSplitResDirs.
在 ResourcesManager.mResourceImpls 中用新的 ResourcesKey 替换所有旧的 ResourcesKey.
调用 ResourcesManager.appendLibAssetForMainAssetPath(appInfo.publicSourceDir, "${packageName}.vastub") 更新所有资源.
参考 android.webkit.WebViewDelegate.addWebViewAssetPath(Context)
下面贴出源码,带有每一步的标注,一步一步地看.
// ResourcesManager.java
@TargetApi(Build.VERSION_CODES.N)
private static Resources createResourcesForN(Context context, String packageName, File apk) throws Exception {
long startTime = System.currentTimeMillis();
String newAssetPath = apk.getAbsolutePath();
ApplicationInfo info = context.getApplicationInfo();
// 宿主的 base.apk 路径
String baseResDir = info.publicSourceDir;
// 1.1 更新 ApplicationInfo.splitSourceDirs
info.splitSourceDirs = append(info.splitSourceDirs, newAssetPath);
// 反射获取 ContextImpl.mPackageInfo
LoadedApk loadedApk = Reflector.with(context).field("mPackageInfo").get();
Reflector rLoadedApk = Reflector.with(loadedApk).field("mSplitResDirs");
// 反射获取 LoadedApk.mSplitResDirs 即所有分包地址 splitResDirs
String[] splitResDirs = rLoadedApk.get();
// 1.2 更新 LoadedApk.mSplitResDirs
rLoadedApk.set(append(splitResDirs, newAssetPath));
// 构建新的 ResourcesManager
final android.app.ResourcesManager resourcesManager = android.app.ResourcesManager.getInstance();
// 获取 ResourcesManager.mResourceImpls
// ArrayMap>
ArrayMap> originalMap = Reflector.with(resourcesManager).field("mResourceImpls").get();
synchronized (resourcesManager) {
HashMap> resolvedMap = new HashMap<>();
// 2.在 ResourcesManager.mResourceImpls 中用新的 ResourcesKey 替换所有旧的 ResourcesKey
if (Build.VERSION.SDK_INT >= 28
|| (Build.VERSION.SDK_INT == 27 && Build.VERSION.PREVIEW_SDK_INT != 0)) { // P Preview
// API 28 - lastest or Android P Preview
ResourcesManagerCompatForP.resolveResourcesImplMap(originalMap, resolvedMap, context, loadedApk);
} else {
// API 24 - 27
ResourcesManagerCompatForN.resolveResourcesImplMap(originalMap, resolvedMap, baseResDir, newAssetPath);
}
originalMap.clear();
originalMap.putAll(resolvedMap);
}
// 3.
// 刷新 ResourcesManager.mResourceImpls 并添加指向插件的 ResourceImpl
android.app.ResourcesManager.getInstance().appendLibAssetForMainAssetPath(baseResDir, packageName + ".vastub");
Resources newResources = context.getResources();
// 4.
// lastly, sync all LoadedPlugin to newResources
// 最后把新的 Resources 同步到所有插件 LoadedPlugin 中
for (LoadedPlugin plugin : PluginManager.getInstance(context).getAllLoadedPlugins()) {
plugin.updateResources(newResources);
}
Log.d(TAG, "createResourcesForN cost time: +" + (System.currentTimeMillis() - startTime) + "ms");
return newResources;
}
Step1
首先看 ApplicationInfo.splitSourceDirs 和 LoadedApk.mSplitResDirs 是啥:
// ApplicationInfo.java
/**
* Full paths to zero or more split APKs, indexed by the same order as {@link #splitNames}.
*/
public String[] splitSourceDirs;
// LoadedApk.java
private String[] mSplitResDirs;
从 ApplicationInfo 注释可以看出 splitSourceDirs 保存该 APP 的所有分包 apk 的绝对路径,可以推断 LoadedApk.mSplitResDirs 也是保存一样的值.
通过反射得到这两个数组然后分别调用 ResourcesManager.append() 把插件 apk 路径添加到这两个数组中.
// ResourcesManager.java
private static String[] append(String[] paths, String newPath) {
if (contains(paths, newPath)) {
return paths;
}
final int newPathsCount = 1 + (paths != null ? paths.length : 0);
// 创建一个新的数组
final String[] newPaths = new String[newPathsCount];
if (paths != null) {
System.arraycopy(paths, 0, newPaths, 0, paths.length);
}
newPaths[newPathsCount - 1] = newPath;
return newPaths;
}
Step2
第二步是最复杂的一步.上面说到这一步要替换 ResourcesManager 中保存 ResourceImpls 的 Map 的 key ,但其实还会生成新的指向插件 apk 的 ResourceImpls .
注意:Android 7.0 之前 Resources 是访问资源的实现类,Android 7.0 之后真正访问资源的是 ResourceImpl,而 Resources 变成一个代理类,所以 ResourcesManager 保存的是 ResourceImpl 而不是 Resources,如果对 ResourceImpl 有所修改,还需要替换的 ResourceImpl 对应的 Resources 持有的 ResourceImpl 引用.
// ResourcesManager.java
/**
* A mapping of ResourceImpls and their configurations. These are heavy weight objects
* which should be reused as much as possible.
*/
private final ArrayMap> mResourceImpls =
new ArrayMap<>();
ResourcesManager.mResourceImpls 保存了所有以ResourcesKey 为 key 的 ResourceImpls.
第二步实现步骤:
- 首先获取 ResourcesManager.ResourceImpls 作为原始 Map 变量 originalMap.
- 创建一个新的 Map 变量 resolvedMap.
- 根据 Android 版本的不同调用 ResourcesManagerCompatForP.resolveResourcesImplMap() 或者 ResourcesManagerCompatForN.resolveResourcesImplMap() 根据 originalMap 往 resolvedMap 中添加 ResourceImpls 对应刷新后的 ResourcesKey.
- 把 originalMap 内容替换成 resolvedMap 的数据.由于 originalMap 一直引用 ResourcesManager.ResourceImpls 没有改变,所以操作 originalMap 相当于操作 ResourcesManager.ResourceImpls,第二步完成.
我们重点看第二步中的第三小步,其他都简单易懂.
API 24 - 27 (Android7.0 - Android8.1)
首先从低版本开始,Android API 在 24 - 27 之间的调用 ResourcesManagerCompatForN.resolveResourcesImplMap().
2.5 ResourcesManagerCompatForN.resolveResourcesImplMap()
// ResourcesManager.java
private static final class ResourcesManagerCompatForN {
@TargetApi(Build.VERSION_CODES.KITKAT)
public static void resolveResourcesImplMap(
Map> originalMap,
Map> resolvedMap,
String baseResDir,
String newAssetPath) throws Exception {
for (Map.Entry> entry : originalMap.entrySet()) {
Log.d(TAG, "resolveResourcesImplMap:" + entry.getKey().mResDir);
ResourcesKey key = entry.getKey();
// 判断该 ResourcesImpl 的资源路径是否指向宿主 APK 路径
if (Objects.equals(key.mResDir, baseResDir)) {
// 把插件的路径添加到宿主的 mResDir 中生成新的 ResourcesKey
resolvedMap.put(new ResourcesKey(key.mResDir,
append(key.mSplitResDirs, newAssetPath),
key.mOverlayDirs,
key.mLibDirs,
key.mDisplayId,
key.mOverrideConfiguration,
key.mCompatInfo), entry.getValue());
} else {
// 非宿主(系统资源)就直接添加到 resolvedMap
resolvedMap.put(key, entry.getValue());
}
}
}
}
// Resourceskey.java
public ResourcesKey(@Nullable String resDir,
@Nullable String[] splitResDirs,
@Nullable String[] overlayDirs,
@Nullable String[] libDirs,
int displayId,
@Nullable Configuration overrideConfig,
@Nullable CompatibilityInfo compatInfo){
// ...
}
首选遍历 originalMap,每次循环都需要判断 ResourcesImpl 是否指向宿主的 APK 路径:
- true: 调用 ResourcesKey 构造方法,其中方法参数 splitResDirs 在原 ResourcesKey.splitResDirs 基础上添加了插件 apk 路径,其余参数保持不变生成新的 ResourcesKey,并把旧的 ResourcesImpl 和新的 ResourcesKey 存到 resolvedMap.
- false: 直接用原本的 key 把 ResourcesKey 存到 resolvedMap.
所以 resolveResourcesImplMap() 的核心逻辑就是从 originalMap 中找到指向宿主的 ResourcesImpl 然后把重新构建新的 ResourcesKey 且把插件 APK 路径添加到参数中,然后把新的 key 和旧的 ResourcesImpl 添加到 resolvedMap.注意这里只是更新了带有插件 APK 路径的 ResourcesKey, ResourcesImpl 没变依然不能访问插件资源,这个问题留着后面解决.
高版本请接着阅读细读 VirtualApk 之资源加载(下)