透彻分析 VirtualApk 之资源加载

透彻分析 VirtualApk 之资源加载

Android小彩虹2021-07-19 11:06:49100A+A-

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<LoadedPlugin> 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() 看似很长,主要分三步:

  1. 根据系统版本的不同构建新的或者复用宿主 AssetManager ,然后通过反射调用 AssetManager.addAssetPath() 添加插件 apk 路径,最后遍历把所有 LoadedPlugins 的插件 APK 路径都调用 AssetManager.addAssetPath() 添加进去.

    • API 1-20: 创建新的 AssetManager 实例.
    • API 21-23: 获取宿主的 AssetManager 实例.
  2. 根据不同厂商创建新的 Resources 实例.

  3. 更新所有 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<ResourcesKey, WeakReference<Resources>>
    Map<Object, WeakReference<Resources>> map = Reflector.with(resManager).field("mActiveResources").get();
    Object key = map.keySet().iterator().next();
    // 把 Resources 添加到 map 中
    map.put(key, new WeakReference<>(resources));
  } catch (Exception e) {
    Log.w(TAG, e);
  }
}
  1. 首先用反射把新的 Resources 替换宿主 ContextImpl.mResources 指向的 Resources 对象.

  2. 用新的 Resources 替换宿主 ContextImpl.mPackageInfo.mResources 指向的对象.

  3. 根据系统版本的不同获取系统的 ResourcesManager.

    • API 1 - 18: 反射获取 ActivityThread.mResourcesManager.

    • API 19 - 23: 直接创建系统 ResourcesManager 实例.

  4. 反射把新的 Resources 添加到 ResourcesManager.mActiveResources.

Android 7.0 以前的 ResourcesManager 会有一个 Map 保存 Resources 的弱引用和他对应的 ResourcesKey.

// ResourcesManager.java (Android 4.4.4)
final ArrayMap<ResourcesKey, WeakReference<Resources> > mActiveResources = new ArrayMap<ResourcesKey, WeakReference<Resources> >();

低版本创建 Resources 并替换的实现到这里结束了,下面从 createResourcesForN() 开始看高版本的实现.


API 24 - lastest (Android7.0 - lastest)

2.4 ResourcesManager.createResourcesForN()

首先看方法注释

/** * Use System Apis to update all existing resources. * <br/> * 1. Update ApplicationInfo.splitSourceDirs and LoadedApk.mSplitResDirs * <br/> * 2. Replace all keys of ResourcesManager.mResourceImpls to new ResourcesKey * <br/> * 3. Use ResourcesManager.appendLibAssetForMainAssetPath(appInfo.publicSourceDir, "${packageName}.vastub") to update all existing resources. * <br/> * <p> * see android.webkit.WebViewDelegate.addWebViewAssetPath(Context) */

使用系统 API 更新所有 Resources (宿主+插件)

  1. 更新 ApplicationInfo.splitSourceDirs 和 LoadedApk.mSplitResDirs.

  2. 在 ResourcesManager.mResourceImpls 中用新的 ResourcesKey 替换所有旧的 ResourcesKey.

  3. 调用 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<ResourcesKey, WeakReference<ResourcesImpl>>
  ArrayMap<ResourcesKey, WeakReference<ResourcesImpl>> originalMap = Reflector.with(resourcesManager).field("mResourceImpls").get();

  synchronized (resourcesManager) {
    HashMap<ResourcesKey, WeakReference<ResourcesImpl>> 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<ResourcesKey, WeakReference<ResourcesImpl>> mResourceImpls =
  new ArrayMap<>();

ResourcesManager.mResourceImpls 保存了所有以ResourcesKey 为 key 的 ResourceImpls.

第二步实现步骤:

  1. 首先获取 ResourcesManager.ResourceImpls 作为原始 Map 变量 originalMap.
  2. 创建一个新的 Map 变量 resolvedMap.
  3. 根据 Android 版本的不同调用 ResourcesManagerCompatForP.resolveResourcesImplMap() 或者 ResourcesManagerCompatForN.resolveResourcesImplMap() 根据 originalMap 往 resolvedMap 中添加 ResourceImpls 对应刷新后的 ResourcesKey.
  4. 把 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<ResourcesKey, WeakReference<ResourcesImpl>> originalMap, Map<ResourcesKey, WeakReference<ResourcesImpl>> resolvedMap, String baseResDir, String newAssetPath) throws Exception {
    for (Map.Entry<ResourcesKey, WeakReference<ResourcesImpl>> 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 没变依然不能访问插件资源,这个问题留着后面解决.

接着看高版本在 Android P 预览版以及 28 以后的版本调用 ResourcesManagerCompatForP.resolveResourcesImplMap().

API 28 - lastest (Android9 - lastest)
2.6 ResourcesManagerCompatForP.resolveResourcesImplMap()
// ResourcesManager.java
private static final class ResourcesManagerCompatForP {
  @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
  public static void resolveResourcesImplMap( Map<ResourcesKey, WeakReference<ResourcesImpl>> originalMap, Map<ResourcesKey, WeakReference<ResourcesImpl>> resolvedMap, Context context, LoadedApk loadedApk) throws Exception {
    HashMap<ResourcesImpl, Context> newResImplMap = new HashMap<>();
    Map<ResourcesImpl, ResourcesKey> resKeyMap = new HashMap<>();
    Resources newRes;

    // Recreate the resImpl of the context

    // See LoadedApk.getResources()
    if (mDefaultConfiguration == null) {
      mDefaultConfiguration = new Configuration();
    }
    // 1.
    newRes = context.createConfigurationContext(mDefaultConfiguration).getResources();
    // 把宿主 ResourcesImpl 添加到 newResImplMap
    newResImplMap.put(newRes.getImpl(), context);

    // 2.
    // Recreate the ResImpl of the activity
    // 获取所有启动过的 Activity 的 ResourcesImpl 并添加到 newResImplMap
    for (WeakReference<Activity> ref : PluginManager.getInstance(context).getInstrumentation().getActivities()) {
      Activity activity = ref.get();
      if (activity != null) {
        // 同上 ContextImpl.createConfigurationContext()
        newRes = activity.createConfigurationContext(activity.getResources().getConfiguration()).getResources();
        newResImplMap.put(newRes.getImpl(), activity);
      }
    }

    // 3.
    // Mapping all resKey and resImpl
    // 遍历原始 Map
    for (Map.Entry<ResourcesKey, WeakReference<ResourcesImpl>> entry : originalMap.entrySet()) {
      ResourcesImpl resImpl = entry.getValue().get();
      if (resImpl != null) {
        // 如果 resImpl != null 保存到 resKeyMap
        resKeyMap.put(resImpl, entry.getKey());
      }
      resolvedMap.put(entry.getKey(), entry.getValue());
    }

    // 4.
    // Replace the resImpl to the new resKey and remove the origin resKey
    for (Map.Entry<ResourcesImpl, Context> entry : newResImplMap.entrySet()) {
      ResourcesKey newKey = resKeyMap.get(entry.getKey());
      ResourcesImpl originResImpl = entry.getValue().getResources().getImpl();

      resolvedMap.put(newKey, new WeakReference<>(originResImpl));
      resolvedMap.remove(resKeyMap.get(originResImpl));
    }
  }
}

ResourcesManagerCompatForP() 实现可以分成四个部分看,已在代码注释标记.

准备工作:

创建 newResImplMap 保存等下重新创建的 ResourcesImpl 和对应的 Context.

创建 resKeyMap 保存更新后的 ResourcesKey 和对应的 ResourcesImpl.

第一步:

从注释可以看到第一步的目的是重新创建 Context 的 ResourcesImpl.并参考 LoadedApk.getResources().

我们看下参考 LoadedApk 的代码.

// LoadedApk.java
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.java
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 {
    // ...
    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 {
    // ...
  }
}

private @Nullable Resources getOrCreateResources(@Nullable IBinder activityToken, @NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
  synchronized (this) {
    // ...
    ResourcesImpl resourcesImpl = createResourcesImpl(key);
    if (resourcesImpl == null) {
      return null;
    }

    // Add this ResourcesImpl to the cache.
    mResourceImpls.put(key, new WeakReference<>(resourcesImpl));

    // ...
    return resources;
  }
}

private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {
  final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration);
  daj.setCompatibilityInfo(key.mCompatInfo);

  // AssetManager 可访问插件的静态资源
  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);

  if (DEBUG) {
    Slog.d(TAG, "- creating impl=" + impl + " with key: " + key);
  }
  return impl;
}

上面贴了 LoadedApk 和 ResourcesManager 的代码.

  • 根据注释参考 LoadedApk.getResources() 的目的是为了重新创建 Context 的 ResourcesImpl (其实还有 ResourcesKey ),所以我们带着这个目的去看代码, LoadedApk.getResources() 主要调用了 ResourcesManager.getInstance().getResources(),查看方法参数注意 splitResDirs, VirtualApk 通过把插件 APK 路径添加到 splitResDirs 来构建新的 ResourcesImpl 和 ResourcesKey ,记住这个下文会如何做到.

  • 接着看 ResourcesManager 的 getResources():

    • 首先构建一个新的 ResourcesKey ,注意到构造函数有参数 splitResDirs ,因为 splitResDirs 改变了所以需要重新构建 ResourcesKey ,所有符合条件的 ResourcesImpl 都要重新构建对应的 ResourcesKey ,也是 ResourcesManager.createResourcesForN() 第二个步骤的目的.
    • 重新创建了 ResourcesKey 后调用 getOrCreateResources() ,然后再调用 createResourcesImpl() 用 ResourcesKey 重建新的 ResourcesImpl,然后把他们保存到 mResourceImpls 中.用 ResourcesKey 重新创建 ResourcesImpl 的目的可以在 createResourcesImpl() 中看到, ResourcesImpl 构造参数中有 AssetManager ,而构造 AssetManager 的需要 ResourcesKey.splitResDirs 所以必须重新创建 ResourcesImpl 对象并更新.
    • ResourcesManager.mResourceImpls 就是 originalMap,所以调用完 ResourcesManager.getInstance().getResources() 后 originalMap 会刷新所有 ResourcesKey 和对应的 ResourcesImpl .ResourcesManager.mResourceImpls 是 ResourcesManager.createResourcesForN() 第三步的关键先生.

到这里我们了解到参考 LoadedApk.getResources() 的目的是调用 ResourcesManager.getInstance().getResources(),那么回到 ResourcesManagerCompatForP() 的实现部分如下:

if (mDefaultConfiguration == null) {
    mDefaultConfiguration = new Configuration();
}
// 首先构建新的 ContextImpl 并调用 setResources() 设置新构建的 Resources
// 再调用 getRessources() 获取新构建的 Resources
newRes = context.createConfigurationContext(mDefaultConfiguration).getResources();
newResImplMap.put(newRes.getImpl(), context);

就是构建了一个空的 Configuration,然后调用 context.createConfigurationContext(),我们看下 Context 实现类 ContextImpl 的实现.

// ContextImpl.java
@Override
public Context createConfigurationContext(Configuration overrideConfiguration) {
	// ...
  ContextImpl context = new ContextImpl(this, mMainThread, mPackageInfo, mSplitName,
                                        mActivityToken, mUser, mFlags, mClassLoader);

  final int displayId = mDisplay != null ? mDisplay.getDisplayId() : Display.DEFAULT_DISPLAY;
  context.setResources(createResources(mActivityToken, mPackageInfo, mSplitName, displayId,
                                       overrideConfiguration,    
                                       getDisplayAdjustments(displayId).getCompatibilityInfo()));
  return context;
}
  • createConfigurationContext() 首先构建一个新的 ContextImpl.
  • 然后调用 createResources() 创建新的 Resources 并调用 setResources() 设置到 ContextImpl 中.下面看下 createResources().
// ContextImpl.java
private static Resources createResources(IBinder activityToken, LoadedApk pi, String splitName, int displayId, Configuration overrideConfig, CompatibilityInfo compatInfo) {
  final String[] splitResDirs;
  final ClassLoader classLoader;
  try {
    splitResDirs = pi.getSplitPaths(splitName);
    classLoader = pi.getSplitClassLoader(splitName);
  } catch (NameNotFoundException e) {
    throw new RuntimeException(e);
  }
  return ResourcesManager.getInstance().getResources(activityToken,
                                                     pi.getResDir(),
                                                     splitResDirs,
                                                     pi.getOverlayDirs(),
                                                     pi.getApplicationInfo().sharedLibraryFiles,
                                                     displayId,
                                                     overrideConfig,
                                                     compatInfo,
                                                     classLoader);
}

到这里我们看到了熟悉的 ResourcesManager.getInstance().getResources() ,这里留意刚才我们提到的参数 splitResDirs ,在这里他来自 ContextImpl.mPackageInfo ,还记得之前 ResourcesManager.createResourcesForN() 第一步中通过反射 ContextImpl.mPackageInfo.splitResDirs 并添加了插件 APK 路径,所以这里的 splitResDirs 已经被修改了,对应上文参考 LoadApk.getResources() 的流程.

createConfigurationContext() 执行完后 originalMap 会添加可访问宿主和插件资源的 ResourcesImpl 和对应的 ResourcesKey.

整个过程中 createConfigurationContext() 构建的 ContextImpl 实例看似是多余的(留个坑后面补上)只是为了调用 ResourcesManager.getInstance().getResources() ,还记得上面 ResourcesManager.createResourcesForN() 的注释说道实现的思想使用系统的 API 来达到我们的目的,个人认为这里直接调用 ResourcesManager.getInstance().getResources() 且需要很多参数明显是不好维护的,万一后续 Android 版本更新可能就要维护另外一套代码,使用系统 API 用 createConfigurationContext() 巧妙地调用 ResourcesManager.getInstance().getResources() 可以达到目的,减少维护成本.

到这里 ResourcesManagerCompatForP() 第一步看完了,我们知道 originalMap 中的 ResourcesKey 和 ResourcesImpl 已经全部刷新且添加了插件的 ResourcesImpl.继续看第二步.

第二步:

为了方便查看直接把 ResourcesManagerCompatForP() 代码重新贴一次.

private static final class ResourcesManagerCompatForP {
  @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
  public static void resolveResourcesImplMap( Map<ResourcesKey, WeakReference<ResourcesImpl>> originalMap, Map<ResourcesKey, WeakReference<ResourcesImpl>> resolvedMap, Context context, LoadedApk loadedApk) throws Exception {
    HashMap<ResourcesImpl, Context> newResImplMap = new HashMap<>();
    Map<ResourcesImpl, ResourcesKey> resKeyMap = new HashMap<>();
    Resources newRes;

    // Recreate the resImpl of the context

    // See LoadedApk.getResources()
    if (mDefaultConfiguration == null) {
      mDefaultConfiguration = new Configuration();
    }
    // 1.
    newRes = context.createConfigurationContext(mDefaultConfiguration).getResources();
    // 把宿主 ResourcesImpl 添加到 newResImplMap
    newResImplMap.put(newRes.getImpl(), context);

    // 2.
    // Recreate the ResImpl of the activity
    // 获取所有启动过的 Activity 的 ResourcesImpl 并添加到 newResImplMap
    for (WeakReference<Activity> ref : PluginManager.getInstance(context).getInstrumentation().getActivities()) {
      Activity activity = ref.get();
      if (activity != null) {
        // 同上 ContextImpl.createConfigurationContext()
        newRes = activity.createConfigurationContext(activity.getResources().getConfiguration()).getResources();
        newResImplMap.put(newRes.getImpl(), activity);
      }
    }

    // 3.
    // Mapping all resKey and resImpl
    // 遍历原始 Map
    for (Map.Entry<ResourcesKey, WeakReference<ResourcesImpl>> entry : originalMap.entrySet()) {
      ResourcesImpl resImpl = entry.getValue().get();
      if (resImpl != null) {
        // 如果 resImpl != null 保存到 resKeyMap
        resKeyMap.put(resImpl, entry.getKey());
      }
      resolvedMap.put(entry.getKey(), entry.getValue());
    }

    // 4.
    // Replace the resImpl to the new resKey and remove the origin resKey
    for (Map.Entry<ResourcesImpl, Context> entry : newResImplMap.entrySet()) {
      ResourcesKey newKey = resKeyMap.get(entry.getKey());
      // 此时新的 ResourcesImpl 对应的 Context 持有的 Resources 引用没有改变
      // 所以通过这个 Resources 可以找到旧的 ResourcesImpl
      ResourcesImpl originResImpl = entry.getValue().getResources().getImpl();
			// 注意添加的是 新的Key - 旧的ResImpl
      resolvedMap.put(newKey, new WeakReference<>(originResImpl));
      resolvedMap.remove(resKeyMap.get(originResImpl));
    }
  }
}

第二步的目的是重新创建 Activity 的 ResourcesImpl,和第一步一样调用 createConfigurationContext() 重新构建新的 ResourcesImpl,然后添加到 newResImplMap.此时 newResImplMap 包含了上面两次调用 createConfigurationContext() 添加的可访问宿主和插件的 ResourcesKey 和 ResourcesImpl.

到这里我们回顾 Step1 和 Step2 ,分别调用了 ContextImpl.createConfigurationContext 和 Activity.createConfigurationContext() ,之前说过 Step1 中的 ContextImpl 是宿主的 Application Context ,虽然前文并没有提到但现在我们可以确定 ResourcesManager.mResourceImpls 保存的是 Application 和 Activity 的 ResourcesImpl ,由于本文篇幅有限有兴趣的同学可以查看 ResourcesManager 源码查看 mResourceImpls 添加元素的过程.

Step3

第三步就是把 originalMap 中所有键值对添加到 resolvedMap, 然后把不为 null 的 ResourcesImpl 和对应的 ResourcesKey 添加到 resKeyMap.

Step4

第四步根据注释是刷新 ResourcesKey ,我们需要清楚当前 originalMap 同时存在了 Application Context 和 Activity 原本就存在的对应的 ResourcesKey-ResourcesImpl 键值对,以及添加了插件 APK 后构建的新的 ResourcesKey-ResourcesImpl 键值对,新旧键值对之间是对应的关系.

第四步的目的是用新的键值对的 key 和对应的旧的键值对的 ResImpl 组成新的键值对存放到 originalMap 中然后抛弃之前的新旧键值对.具体实现细节看源码即可理解.

小结

现在我们已经看完高版本 resolveResourcesImplMap() 的实现,我们知道经过刚才四步对 originalMap 做了修改, 此时 originalMap 中可访问宿主 APK 资源的 ResourceImpls 对应的 ResourcesKey 被新的 key 所替代,且新的 key 带有插件 APK 路径,也就是说现在只是刷新了 ResourcesKey , 我们知道 Android 应用层通常是用 Resources 来间接操作 ResourcesImpl 访问 APK 资源的,所以我们还需要重新构建可访问宿主和插件 APK 资源的 ResourcesImpl 同时刷新当前应用中所有 Resources 引用持有的 ResourcesImpl 变量.

现在回到 createResourcesForN() ,下面截取 createResourcesForN() 后半段代码:

// ResourcesManager.java 
private static Resources createResourcesForN(Context context, String packageName, File apk) throws Exception {
  // ...
  synchronized (resourcesManager) {
    HashMap<ResourcesKey, WeakReference<ResourcesImpl>> resolvedMap = new HashMap<>();
    if (Build.VERSION.SDK_INT >= 28
        || (Build.VERSION.SDK_INT == 27 && Build.VERSION.PREVIEW_SDK_INT != 0)) { // P Preview
      ResourcesManagerCompatForP.resolveResourcesImplMap(originalMap, resolvedMap, context, loadedApk);
    } else {
      ResourcesManagerCompatForN.resolveResourcesImplMap(originalMap, resolvedMap, baseResDir, newAssetPath);
    }
    originalMap.clear();
    originalMap.putAll(resolvedMap);
    printMapKey(originalMap);
  }
  
  android.app.ResourcesManager.getInstance().appendLibAssetForMainAssetPath(baseResDir, packageName + ".vastub");
  
  Resources newResources = context.getResources();
  for (LoadedPlugin plugin : PluginManager.getInstance(context).getAllLoadedPlugins()) {
    plugin.updateResources(newResources);
  }
  return newResources;
}
  • 根据版本调用不同的 resolveResourcesImplMap() 后会用 originalMap 清空数据并添加 resolvedMap 的数据,
  • 然后调用系统 ResourcesManager 的 appendLibAssetForMainAssetPath().
  • 获取宿主 Application Context 的 Resources 对象并用它替换所有 LoadedPlugin 的 Resources.

上面说到我们还要更新 Resources ,可以猜测 appendLibAssetForMainAssetPath() 会利用 ResourcesManager.mResourceImpls 构建新的宿主的 Resources .为了验证猜想从 appendLibAssetForMainAssetPath() 开始阅读.

注意这里调用的不是 VirtualApk 的 ResourcesManager 而是 Android 系统的 ResourcesManager.

3. Android 系统的 ResourcesManager

Resources 的管理类,采用单例模式实现,所有 Resources 的创建、缓存、删除都由 ResourcesManager 来管理.

3.1 ResourcesManager.appendLibAssetForMainAssetPath()
// ResourcesManager.java
/** * Appends the library asset path to any ResourcesImpl object that contains the main * assetPath. * @param assetPath The main asset path for which to add the library asset path. * @param libAsset The library asset path to add. */
public void appendLibAssetForMainAssetPath(String assetPath, String libAsset) {
  synchronized (this) {
    // Record which ResourcesImpl need updating
    // (and what ResourcesKey they should update to).
    final ArrayMap<ResourcesImpl, ResourcesKey> updatedResourceKeys = new ArrayMap<>();

    final int implCount = mResourceImpls.size();
    // 遍历 mResourceImpls
    for (int i = 0; i < implCount; i++) {
      final ResourcesKey key = mResourceImpls.keyAt(i);
      final WeakReference<ResourcesImpl> weakImplRef = mResourceImpls.valueAt(i);
      final ResourcesImpl impl = weakImplRef != null ? weakImplRef.get() : null;
      // key.mResDir 包含宿主 apk 路径
      if (impl != null && Objects.equals(key.mResDir, assetPath)) {
        // key.mLibDirs 不包含 "插件包名.vastub"
        if (!ArrayUtils.contains(key.mLibDirs, libAsset)) {
          // 新建一个数组 newLibAssets 添加 key.mLibDirs 内容和 "插件包名.vastub"
          final int newLibAssetCount = 1 +
            (key.mLibDirs != null ? key.mLibDirs.length : 0);
          final String[] newLibAssets = new String[newLibAssetCount];
          if (key.mLibDirs != null) {
            System.arraycopy(key.mLibDirs, 0, newLibAssets, 0, key.mLibDirs.length);
          }
          newLibAssets[newLibAssetCount - 1] = libAsset;

          // 构建新的 ResourcesKey 放进 updatedResourceKeys
          updatedResourceKeys.put(impl, new ResourcesKey(
            key.mResDir,
            key.mSplitResDirs,
            key.mOverlayDirs,
            newLibAssets,
            key.mDisplayId,
            key.mOverrideConfiguration,
            key.mCompatInfo));
        }
      }
    }
    redirectResourcesToNewImplLocked(updatedResourceKeys);
  }
}

根据方法注释我们知道 appendLibAssetForMainAssetPath() 会根据方法参数 assetPath 找到所有符合要求的 ResourcesImpl 然后在他的 asset 路径中添加第二个参数 libAsset.

PS:上面代码注释从本文角度添加的.

方法逻辑如下:

  • 遍历 ResourcesManager.mResourcesImpls ,寻找符合以下要求的 ResourcesKey.
    • ResourcesKey.mResDir 数组包含宿主 apk 路径.
    • ResourcesKey.mLibDirs 数组不包含不包含字符串:插件包名.vastub.
  • 为符合要求的 ResourcesKey 创建新的数组添加 ResourcesKey.mLibDirs 原有数据和字符串插件包名.vastub.
  • 用新的数组和 ResourcesKey 原有属性重新生成一个 ResourcesKey 并添加到 updatedResourceKeys 中.
  • 最后调用 redirectResourcesToNewImplLocked().

很明显符合这两个要求的 ResourcesKey 对应的是刚才 resolveResourcesImplMap() 创建的可访问宿主和插件资源的 ResourcesImpl ,接着看 ResourcesManager.redirectResourcesToNewImplLocked().

注意此时只是新建了 ResourcesKey 并保存它以及对应的 ResourcesImpl 到 updatedResourceKeys ,并没有改变 ResourcesManager.mResourceImpls 中的值.

3.2 ResourcesManager.redirectResourcesToNewImplLocked()
// ResourcesManager.java
private void redirectResourcesToNewImplLocked( @NonNull final ArrayMap<ResourcesImpl, ResourcesKey> updatedResourceKeys) {
  // Bail early if there is no work to do.
  if (updatedResourceKeys.isEmpty()) {
    return;
  }

  // Update any references to ResourcesImpl that require reloading.
  // mResourceReferences 保存了所有 Resources 弱引用
  final int resourcesCount = mResourceReferences.size();
  for (int i = 0; i < resourcesCount; i++) {
    final WeakReference<Resources> ref = mResourceReferences.get(i);
    final Resources r = ref != null ? ref.get() : null;
    if (r != null) {
      // 获取新的 key
      final ResourcesKey key = updatedResourceKeys.get(r.getImpl());
      if (key != null) {
        // 新的 key 获取或者重新创建一个 ResourcesImpl
        final ResourcesImpl impl = findOrCreateResourcesImplForKeyLocked(key);
        if (impl == null) {
          throw new Resources.NotFoundException("failed to redirect ResourcesImpl");
        }
        // 设置给 Resources
        r.setImpl(impl);
      }
    }
  }

  // Update any references to ResourcesImpl that require reloading for each Activity.
  // activityResources 保存了所有与 Activity 关联的 Resources
  // ActivityResource 持有关联的 Resources 弱引用
  for (ActivityResources activityResources : mActivityResourceReferences.values()) {
    final int resCount = activityResources.activityResources.size();
    for (int i = 0; i < resCount; i++) {
      final WeakReference<Resources> ref = activityResources.activityResources.get(i);
      final Resources r = ref != null ? ref.get() : null;
      if (r != null) {
        final ResourcesKey key = updatedResourceKeys.get(r.getImpl());
        if (key != null) {
          final ResourcesImpl impl = findOrCreateResourcesImplForKeyLocked(key);
          if (impl == null) {
            throw new Resources.NotFoundException(
              "failed to redirect ResourcesImpl");
          }
          r.setImpl(impl);
        }
      }
    }
  }
}

在 ResourcesManager 中 mResourceReferences 和 mActivityResourceReferences 保存了所有 Resources 的弱引用, redirectResourcesToNewImplLocked() 主要逻辑:

  • 遍历这两个集合中所有的 Resources ,用 Resources 持有的 ResourcesImpl 对象从 updatedResourceKeys 找到对应的新的 ResourcesKey.
  • 调用 findOrCreateResourcesImplForKeyLocked() 重新创建新的 ResourcesImpl 并添加到 ResourcesManager.mResourceImpls 中.
  • 最后把新建的 ResourcesImpl 设置到 Resources.

心思缜密的同学可能会发现一个疑点:

updatedResourceKeys 中的 ResourcesImpl 是可以访问宿主和插件资源的,上文明明说道当前宿主的 Resources 并没有更新它持有的 ResourcesImpl ,为什么可以从这两个集合中找到 Resources 持有可访问宿主插件资源的 ResourcesImpl 呢?

答案就在之前说到的 createConfigurationContext() 中,之前并没有太仔细阅读他的调用过程,我之前说过他创建的 Context 看似是没有用的,但不要忘了这个方法内会构建新的 Resources ,调用链会调用 getOrCreateResourcesLocked() ,而在这个方法内就会把这个 Resources 添加到 mResourceReferences ,所以这个疑点解开了.这样也说明 Android 系统中创建的所有 Resources 都会缓存在 ResourcesManager.mResourceReferences 中.


由于上面的过程比较复杂,为了方便理解我下面以高版本 resolveResourcesImplMap() 为例子,打印了 ResourcesManager.mResourcesImpls 键值对以及 ResourcesKey.mLibDirs 和 ResourcesKey.mLibDirs 的变化,大家可以根据 hasCode 确定 ResourcesKey 和 ResourcesImpl 对象是否相同.我也加上了注释展示变化过程.

其中 Test.apk 是 VirtualApk demo 中的插件 APK ,包名为 com.didi.virtualapk.demo

-----------------------------------------------------------------
Original:
ResourcesKey hashCode:-37900270;ResourcesImpl hashCode:184117293

ResourcesKey hashCode:-1609252614;ResourcesImpl hashCode:33820484
ResourcesKey.mLibDirs:[/system/framework/org.apache.http.legacy.boot.jar]

ResourcesKey hashCode:1739974881;ResourcesImpl hashCode:163397986
ResourcesKey.mLibDirs:[/system/framework/org.apache.http.legacy.boot.jar]
-----------------------------------------------------------------
after contextImpl.createConfigurationContext():
ResourcesKey hashCode:-37900270;ResourcesImpl hashCode:184117293

// Application Context 旧 ResourcesKey-ResourcesImpl
ResourcesKey hashCode:-1609252614;ResourcesImpl hashCode:33820484
ResourcesKey.mLibDirs:[/system/framework/org.apache.http.legacy.boot.jar]

// Activity 旧 ResourcesKey-ResourcesImpl
ResourcesKey hashCode:1739974881;ResourcesImpl hashCode:163397986
ResourcesKey.mLibDirs:[/system/framework/org.apache.http.legacy.boot.jar]

// Application Context 新增 ResourcesKey-ResourcesImpl
ResourcesKey hashCode:-518486335;ResourcesImpl hashCode:55370483
ResourcesKey.mSplitResDirs:[/storage/emulated/0/Test.apk]
ResourcesKey.mLibDirs:[/system/framework/org.apache.http.legacy.boot.jar]
-----------------------------------------------------------------
after Activity.createConfigurationContext():
ResourcesKey hashCode:-37900270;ResourcesImpl hashCode:184117293

// Application Context 旧 ResourcesKey-ResourcesImpl
ResourcesKey hashCode:-1609252614;ResourcesImpl hashCode:33820484
ResourcesKey.mLibDirs:[/system/framework/org.apache.http.legacy.boot.jar]

// Activity 旧 ResourcesKey-ResourcesImpl
ResourcesKey hashCode:1739974881;ResourcesImpl hashCode:163397986
ResourcesKey.mLibDirs:[/system/framework/org.apache.http.legacy.boot.jar]

// Application Context 新增 ResourcesKey-ResourcesImpl
ResourcesKey hashCode:-518486335;ResourcesImpl hashCode:55370483
ResourcesKey.mSplitResDirs:[/storage/emulated/0/Test.apk]
ResourcesKey.mLibDirs:[/system/framework/org.apache.http.legacy.boot.jar]

// Activity 新增 ResourcesKey-ResourcesImpl
ResourcesKey hashCode:-1464226136;ResourcesImpl hashCode:265608112
ResourcesKey.mSplitResDirs:[/storage/emulated/0/Test.apk]
ResourcesKey.mLibDirs:[/system/framework/org.apache.http.legacy.boot.jar]

-----------------------------------------------------------------
Update keys:
ResourcesKey hashCode:-37900270;ResourcesImpl hashCode:184117293

// Application Context 新增 ResourcesKey-ResourcesImpl
// 删除旧键值对后用就键值对的 ResourcesImpl 刷新新的 ResourcesImpl
ResourcesKey hashCode:-518486335;ResourcesImpl hashCode:33820484
ResourcesKey.mSplitResDirs:[/storage/emulated/0/Test.apk]
ResourcesKey.mLibDirs:[/system/framework/org.apache.http.legacy.boot.jar]

// Activity 新增 ResourcesKey-ResourcesImpl
// 删除旧键值对后用就键值对的 ResourcesImpl 刷新新的 ResourcesImpl
ResourcesKey hashCode:-1464226136;ResourcesImpl hashCode:163397986
ResourcesKey.mSplitResDirs:[/storage/emulated/0/Test.apk]
ResourcesKey.mLibDirs:[/system/framework/org.apache.http.legacy.boot.jar]
-----------------------------------------------------------------
After appendLibAssetForMainAssetPath():
ResourcesKey hashCode:-37900270;ResourcesImpl hashCode:184117293

// Application Context 新增 ResourcesKey-ResourcesImpl
ResourcesKey hashCode:-518486335;ResourcesImpl hashCode:33820484
ResourcesKey.mSplitResDirs:[/storage/emulated/0/Test.apk]
ResourcesKey.mLibDirs:[/system/framework/org.apache.http.legacy.boot.jar]

// Activity 新增 ResourcesKey-ResourcesImpl
ResourcesKey hashCode:-1464226136;ResourcesImpl hashCode:163397986
ResourcesKey.mSplitResDirs:[/storage/emulated/0/Test.apk]
ResourcesKey.mLibDirs:[/system/framework/org.apache.http.legacy.boot.jar]

// Resources 绑定的带有 .vastub 标记的对应 Application Context 的 ResourcesKey-ResourcesImpl
ResourcesKey hashCode:854922412;ResourcesImpl hashCode:45177385
ResourcesKey.mSplitResDirs:[/storage/emulated/0/Test.apk]
ResourcesKey.mLibDirs:[/system/framework/org.apache.http.legacy.boot.jar,com.didi.virtualapk.demo.vastub]

// Resources 绑定的带有 .vastub 标记的对应 Activity 的 ResourcesKey-ResourcesImpl
ResourcesKey hashCode:1800662213;ResourcesImpl hashCode:62001582
ResourcesKey.mSplitResDirs:[/storage/emulated/0/Test.apk]
ResourcesKey.mLibDirs:[/system/framework/org.apache.http.legacy.boot.jar,com.didi.virtualapk.demo.vastub]
-----------------------------------------------------------------

到这里 Resources 插件化的过程已经完成了,当前 APP 可以访问插件资源,但是在 createResourcesForN() 中有一个小细节需要我们回顾一下:

调用 appendLibAssetForMainAssetPath() 完成后还有一步操作就是更新多有 LoadedPlugin 的 Resources ,但是注意到使用宿主 Application Context 的 Resources 来做替换的,虽然并没有提到为啥要用 Application 的,但其实从刚才的 appendLibAssetForMainAssetPath() 就能找到答案.

回顾之前小结说道 ResourcesManager.mResourcesImpls 在调用 appendLibAssetForMainAssetPath() 之前刷新了可访问宿主 APK 资源的 ResourcesImpl 对应的 ResoucesKey ,需要构建可访问新的 ResourcesImpl 并设置到所有 Resouces 中,这不正是 redirectResourcesToNewImplLocked() 做事情吗,恍然大悟后我们记得 Application Context 对应的 Resources 就保存在 ResourcesManager.mResourceReferences 中,所以在 redirectResourcesToNewImplLocked() 中就已经刷新了 Application Context 的 Resources 对应的 ResoucesImpl ,补全了这个小细节.

// ResourcesManager.java 
private static Resources createResourcesForN(Context context, String packageName, File apk) throws Exception {
  // ...
  android.app.ResourcesManager.getInstance().appendLibAssetForMainAssetPath(baseResDir, packageName + ".vastub");
  
  Resources newResources = context.getResources();
  for (LoadedPlugin plugin : PluginManager.getInstance(context).getAllLoadedPlugins()) {
    plugin.updateResources(newResources);
  }
  return newResources;
}

总结

VirtualApk 加载插件资源流程总结如下:

API 1 - 23

  • 根据版本新建或用原有的 AM 并把插件 APK 路径添加到 AM 中:
    • 1-20 : 构建新的 AssetManager ,并把宿主 AssetManager 资源路径添加到新的 AM 中
    • 21-23: 用宿主的 AssetManager.
  • 用新的 AM 构建新的 Resources (适配部分厂商).
  • 更新所有 LoadedPlugin 的 Resources.
  • Hook 当前进程用新的 Resources 代替.
    • 替换宿主 Context 的 Resources.
    • 替换 ContextImpl.mPackageInfo.mResources.
    • 根据版本获取 ResourcesManager 并把新的 Resources 缓存到 ResourcesManager.mActiveResources.
      • 1-18: 反射获取 ActivityThread.mResourcesManager.
      • 19-23: 创建新的 ResourcesManager 实例.

API 24 - lastest

  • 反射 ContextImpl.mPackageInfo.mSplitResDirs 添加插件 APK 路径.
  • 根据版本刷新 ResourcesManager.mResourceImpls 中可加载宿主资源的 ResourcesImpl 对应的 ResourcesKey.
    • 24-27: 直接生成新的带有插件 APk 路径的 ResoucesKey 并刷新.
    • 28-lastest: 太复杂了不写了.
  • 重新创建所有 Resources 实例(包括 Application 和 Activity)对应的 ResourcesImpl 为可访问宿主资源的 ResourcesImpl 并替代 Resources.mResourcesImpl.
  • 更新所有 LoadedPlugin 的 Resources.

参考资料

Android 资源加载机制剖析

Android源码分析-资源加载机制

聊聊Android中的ContextImpl

点击这里复制本文地址 以上内容由权冠洲的博客整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!

支持Ctrl+Enter提交

联系我们| 本站介绍| 留言建议 | 交换友链 | 域名展示
本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除

权冠洲的博客 © All Rights Reserved.  Copyright quanguanzhou.top All Rights Reserved
苏公网安备 32030302000848号   苏ICP备20033101号-1

联系我们