细读 VirtualApk 之资源加载(上)

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

  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>
    Map> 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 > 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 (宿主+插件)

  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>
  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.

第二步实现步骤:

  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> 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 之资源加载(下)

你可能感兴趣的:(细读 VirtualApk 之资源加载(上))