细读 VirtualApk 之资源加载(下)

接着细读 VirtualApk 之资源加载(上)
看高版本在 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> originalMap, 
    Map> resolvedMap, 
    Context context, LoadedApk loadedApk) throws Exception {
    HashMap newResImplMap = new HashMap<>();
    Map 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 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> 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 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> originalMap, 
    Map> resolvedMap, 
    Context context, LoadedApk loadedApk) throws Exception {
    HashMap newResImplMap = new HashMap<>();
    Map 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 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> 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 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> 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 updatedResourceKeys = new ArrayMap<>();

    final int implCount = mResourceImpls.size();
    // 遍历 mResourceImpls
    for (int i = 0; i < implCount; i++) {
      final ResourcesKey key = mResourceImpls.keyAt(i);
      final WeakReference 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 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 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 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

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