細(xì)讀 VirtualApk 之資源加載(下)

接著細(xì)讀 VirtualApk 之資源加載(上)
看高版本在 Android P 預(yù)覽版以及 28 以后的版本調(diào)用 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() 實現(xiàn)可以分成四個部分看,已在代碼注釋標(biāo)記.

準(zhǔn)備工作:

創(chuàng)建 newResImplMap 保存等下重新創(chuàng)建的 ResourcesImpl 和對應(yīng)的 Context.

創(chuàng)建 resKeyMap 保存更新后的 ResourcesKey 和對應(yīng)的 ResourcesImpl.

第一步:

從注釋可以看到第一步的目的是重新創(chuàng)建 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 可訪問插件的靜態(tài)資源
  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 的代碼.

  • 根據(jù)注釋參考 LoadedApk.getResources() 的目的是為了重新創(chuàng)建 Context 的 ResourcesImpl (其實還有 ResourcesKey ),所以我們帶著這個目的去看代碼, LoadedApk.getResources() 主要調(diào)用了 ResourcesManager.getInstance().getResources(),查看方法參數(shù)注意 splitResDirs, VirtualApk 通過把插件 APK 路徑添加到 splitResDirs 來構(gòu)建新的 ResourcesImpl 和 ResourcesKey ,記住這個下文會如何做到.

  • 接著看 ResourcesManager 的 getResources():

    • 首先構(gòu)建一個新的 ResourcesKey ,注意到構(gòu)造函數(shù)有參數(shù) splitResDirs ,因為 splitResDirs 改變了所以需要重新構(gòu)建 ResourcesKey ,所有符合條件的 ResourcesImpl 都要重新構(gòu)建對應(yīng)的 ResourcesKey ,也是 ResourcesManager.createResourcesForN() 第二個步驟的目的.
    • 重新創(chuàng)建了 ResourcesKey 后調(diào)用 getOrCreateResources() ,然后再調(diào)用 createResourcesImpl() 用 ResourcesKey 重建新的 ResourcesImpl,然后把他們保存到 mResourceImpls 中.用 ResourcesKey 重新創(chuàng)建 ResourcesImpl 的目的可以在 createResourcesImpl() 中看到, ResourcesImpl 構(gòu)造參數(shù)中有 AssetManager ,而構(gòu)造 AssetManager 的需要 ResourcesKey.splitResDirs 所以必須重新創(chuàng)建 ResourcesImpl 對象并更新.
    • ResourcesManager.mResourceImpls 就是 originalMap,所以調(diào)用完 ResourcesManager.getInstance().getResources() 后 originalMap 會刷新所有 ResourcesKey 和對應(yīng)的 ResourcesImpl .ResourcesManager.mResourceImpls 是 ResourcesManager.createResourcesForN() 第三步的關(guān)鍵先生.

到這里我們了解到參考 LoadedApk.getResources() 的目的是調(diào)用 ResourcesManager.getInstance().getResources(),那么回到 ResourcesManagerCompatForP() 的實現(xiàn)部分如下:

if (mDefaultConfiguration == null) {
    mDefaultConfiguration = new Configuration();
}
// 首先構(gòu)建新的 ContextImpl 并調(diào)用 setResources() 設(shè)置新構(gòu)建的 Resources
// 再調(diào)用 getRessources() 獲取新構(gòu)建的 Resources
newRes = context.createConfigurationContext(mDefaultConfiguration).getResources();
newResImplMap.put(newRes.getImpl(), context);

就是構(gòu)建了一個空的 Configuration,然后調(diào)用 context.createConfigurationContext(),我們看下 Context 實現(xiàn)類 ContextImpl 的實現(xiàn).

// 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() 首先構(gòu)建一個新的 ContextImpl.
  • 然后調(diào)用 createResources() 創(chuàng)建新的 Resources 并調(diào)用 setResources() 設(shè)置到 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() ,這里留意剛才我們提到的參數(shù) splitResDirs ,在這里他來自 ContextImpl.mPackageInfo ,還記得之前 ResourcesManager.createResourcesForN() 第一步中通過反射 ContextImpl.mPackageInfo.splitResDirs 并添加了插件 APK 路徑,所以這里的 splitResDirs 已經(jīng)被修改了,對應(yīng)上文參考 LoadApk.getResources() 的流程.

createConfigurationContext() 執(zhí)行完后 originalMap 會添加可訪問宿主和插件資源的 ResourcesImpl 和對應(yīng)的 ResourcesKey.

整個過程中 createConfigurationContext() 構(gòu)建的 ContextImpl 實例看似是多余的(留個坑后面補上)只是為了調(diào)用 ResourcesManager.getInstance().getResources() ,還記得上面 ResourcesManager.createResourcesForN() 的注釋說道實現(xiàn)的思想使用系統(tǒng)的 API 來達到我們的目的,個人認(rèn)為這里直接調(diào)用 ResourcesManager.getInstance().getResources() 且需要很多參數(shù)明顯是不好維護的,萬一后續(xù) Android 版本更新可能就要維護另外一套代碼,使用系統(tǒng) API 用 createConfigurationContext() 巧妙地調(diào)用 ResourcesManager.getInstance().getResources() 可以達到目的,減少維護成本.

到這里 ResourcesManagerCompatForP() 第一步看完了,我們知道 originalMap 中的 ResourcesKey 和 ResourcesImpl 已經(jīng)全部刷新且添加了插件的 ResourcesImpl.繼續(xù)看第二步.

第二步:

為了方便查看直接把 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 對應(yīng)的 Context 持有的 Resources 引用沒有改變
      // 所以通過這個 Resources 可以找到舊的 ResourcesImpl
      ResourcesImpl originResImpl = entry.getValue().getResources().getImpl();
            // 注意添加的是 新的Key - 舊的ResImpl
      resolvedMap.put(newKey, new WeakReference<>(originResImpl));
      resolvedMap.remove(resKeyMap.get(originResImpl));
    }
  }
}

第二步的目的是重新創(chuàng)建 Activity 的 ResourcesImpl,和第一步一樣調(diào)用 createConfigurationContext() 重新構(gòu)建新的 ResourcesImpl,然后添加到 newResImplMap.此時 newResImplMap 包含了上面兩次調(diào)用 createConfigurationContext() 添加的可訪問宿主和插件的 ResourcesKey 和 ResourcesImpl.

到這里我們回顧 Step1 和 Step2 ,分別調(diào)用了 ContextImpl.createConfigurationContext 和 Activity.createConfigurationContext() ,之前說過 Step1 中的 ContextImpl 是宿主的 Application Context ,雖然前文并沒有提到但現(xiàn)在我們可以確定 ResourcesManager.mResourceImpls 保存的是 Application 和 Activity 的 ResourcesImpl ,由于本文篇幅有限有興趣的同學(xué)可以查看 ResourcesManager 源碼查看 mResourceImpls 添加元素的過程.

Step3

第三步就是把 originalMap 中所有鍵值對添加到 resolvedMap, 然后把不為 null 的 ResourcesImpl 和對應(yīng)的 ResourcesKey 添加到 resKeyMap.

Step4

第四步根據(jù)注釋是刷新 ResourcesKey ,我們需要清楚當(dāng)前 originalMap 同時存在了 Application Context 和 Activity 原本就存在的對應(yīng)的 ResourcesKey-ResourcesImpl 鍵值對,以及添加了插件 APK 后構(gòu)建的新的 ResourcesKey-ResourcesImpl 鍵值對,新舊鍵值對之間是對應(yīng)的關(guān)系.

第四步的目的是用新的鍵值對的 key 和對應(yīng)的舊的鍵值對的 ResImpl 組成新的鍵值對存放到 originalMap 中然后拋棄之前的新舊鍵值對.具體實現(xiàn)細(xì)節(jié)看源碼即可理解.

小結(jié)

現(xiàn)在我們已經(jīng)看完高版本 resolveResourcesImplMap() 的實現(xiàn),我們知道經(jīng)過剛才四步對 originalMap 做了修改, 此時 originalMap 中可訪問宿主 APK 資源的 ResourceImpls 對應(yīng)的 ResourcesKey 被新的 key 所替代,且新的 key 帶有插件 APK 路徑,也就是說現(xiàn)在只是刷新了 ResourcesKey , 我們知道 Android 應(yīng)用層通常是用 Resources 來間接操作 ResourcesImpl 訪問 APK 資源的,所以我們還需要重新構(gòu)建可訪問宿主和插件 APK 資源的 ResourcesImpl 同時刷新當(dāng)前應(yīng)用中所有 Resources 引用持有的 ResourcesImpl 變量.

現(xiàn)在回到 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;
}
  • 根據(jù)版本調(diào)用不同的 resolveResourcesImplMap() 后會用 originalMap 清空數(shù)據(jù)并添加 resolvedMap 的數(shù)據(jù),
  • 然后調(diào)用系統(tǒng) ResourcesManager 的 appendLibAssetForMainAssetPath().
  • 獲取宿主 Application Context 的 Resources 對象并用它替換所有 LoadedPlugin 的 Resources.

上面說到我們還要更新 Resources ,可以猜測 appendLibAssetForMainAssetPath() 會利用 ResourcesManager.mResourceImpls 構(gòu)建新的宿主的 Resources .為了驗證猜想從 appendLibAssetForMainAssetPath() 開始閱讀.

注意這里調(diào)用的不是 VirtualApk 的 ResourcesManager 而是 Android 系統(tǒng)的 ResourcesManager.

3. Android 系統(tǒng)的 ResourcesManager

Resources 的管理類,采用單例模式實現(xiàn),所有 Resources 的創(chuàng)建、緩存、刪除都由 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)) {
          // 新建一個數(shù)組 newLibAssets 添加 key.mLibDirs 內(nèi)容和 "插件包名.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;

          // 構(gòu)建新的 ResourcesKey 放進 updatedResourceKeys
          updatedResourceKeys.put(impl, new ResourcesKey(
            key.mResDir,
            key.mSplitResDirs,
            key.mOverlayDirs,
            newLibAssets,
            key.mDisplayId,
            key.mOverrideConfiguration,
            key.mCompatInfo));
        }
      }
    }
    redirectResourcesToNewImplLocked(updatedResourceKeys);
  }
}

根據(jù)方法注釋我們知道 appendLibAssetForMainAssetPath() 會根據(jù)方法參數(shù) assetPath 找到所有符合要求的 ResourcesImpl 然后在他的 asset 路徑中添加第二個參數(shù) libAsset.

PS:上面代碼注釋從本文角度添加的.

方法邏輯如下:

  • 遍歷 ResourcesManager.mResourcesImpls ,尋找符合以下要求的 ResourcesKey.
    • ResourcesKey.mResDir 數(shù)組包含宿主 apk 路徑.
    • ResourcesKey.mLibDirs 數(shù)組不包含不包含字符串:插件包名.vastub.
  • 為符合要求的 ResourcesKey 創(chuàng)建新的數(shù)組添加 ResourcesKey.mLibDirs 原有數(shù)據(jù)和字符串插件包名.vastub.
  • 用新的數(shù)組和 ResourcesKey 原有屬性重新生成一個 ResourcesKey 并添加到 updatedResourceKeys 中.
  • 最后調(diào)用 redirectResourcesToNewImplLocked().

很明顯符合這兩個要求的 ResourcesKey 對應(yīng)的是剛才 resolveResourcesImplMap() 創(chuàng)建的可訪問宿主和插件資源的 ResourcesImpl ,接著看 ResourcesManager.redirectResourcesToNewImplLocked().

注意此時只是新建了 ResourcesKey 并保存它以及對應(yīng)的 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 獲取或者重新創(chuàng)建一個 ResourcesImpl
        final ResourcesImpl impl = findOrCreateResourcesImplForKeyLocked(key);
        if (impl == null) {
          throw new Resources.NotFoundException("failed to redirect ResourcesImpl");
        }
        // 設(shè)置給 Resources
        r.setImpl(impl);
      }
    }
  }

  // Update any references to ResourcesImpl that require reloading for each Activity.
  // activityResources 保存了所有與 Activity 關(guān)聯(lián)的 Resources
  // ActivityResource 持有關(guān)聯(lián)的 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 找到對應(yīng)的新的 ResourcesKey.
  • 調(diào)用 findOrCreateResourcesImplForKeyLocked() 重新創(chuàng)建新的 ResourcesImpl 并添加到 ResourcesManager.mResourceImpls 中.
  • 最后把新建的 ResourcesImpl 設(shè)置到 Resources.

心思縝密的同學(xué)可能會發(fā)現(xiàn)一個疑點:

updatedResourceKeys 中的 ResourcesImpl 是可以訪問宿主和插件資源的,上文明明說道當(dāng)前宿主的 Resources 并沒有更新它持有的 ResourcesImpl ,為什么可以從這兩個集合中找到 Resources 持有可訪問宿主插件資源的 ResourcesImpl 呢?

答案就在之前說到的 createConfigurationContext() 中,之前并沒有太仔細(xì)閱讀他的調(diào)用過程,我之前說過他創(chuàng)建的 Context 看似是沒有用的,但不要忘了這個方法內(nèi)會構(gòu)建新的 Resources ,調(diào)用鏈會調(diào)用 getOrCreateResourcesLocked() ,而在這個方法內(nèi)就會把這個 Resources 添加到 mResourceReferences ,所以這個疑點解開了.這樣也說明 Android 系統(tǒng)中創(chuàng)建的所有 Resources 都會緩存在 ResourcesManager.mResourceReferences 中.


由于上面的過程比較復(fù)雜,為了方便理解我下面以高版本 resolveResourcesImplMap() 為例子,打印了 ResourcesManager.mResourcesImpls 鍵值對以及 ResourcesKey.mLibDirs 和 ResourcesKey.mLibDirs 的變化,大家可以根據(jù) 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 標(biāo)記的對應(yīng) 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 標(biāo)記的對應(yīng) 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 插件化的過程已經(jīng)完成了,當(dāng)前 APP 可以訪問插件資源,但是在 createResourcesForN() 中有一個小細(xì)節(jié)需要我們回顧一下:

調(diào)用 appendLibAssetForMainAssetPath() 完成后還有一步操作就是更新多有 LoadedPlugin 的 Resources ,但是注意到使用宿主 Application Context 的 Resources 來做替換的,雖然并沒有提到為啥要用 Application 的,但其實從剛才的 appendLibAssetForMainAssetPath() 就能找到答案.

回顧之前小結(jié)說道 ResourcesManager.mResourcesImpls 在調(diào)用 appendLibAssetForMainAssetPath() 之前刷新了可訪問宿主 APK 資源的 ResourcesImpl 對應(yīng)的 ResoucesKey ,需要構(gòu)建可訪問新的 ResourcesImpl 并設(shè)置到所有 Resouces 中,這不正是 redirectResourcesToNewImplLocked() 做事情嗎,恍然大悟后我們記得 Application Context 對應(yīng)的 Resources 就保存在 ResourcesManager.mResourceReferences 中,所以在 redirectResourcesToNewImplLocked() 中就已經(jīng)刷新了 Application Context 的 Resources 對應(yīng)的 ResoucesImpl ,補全了這個小細(xì)節(jié).

// 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;
}

總結(jié)

VirtualApk 加載插件資源流程總結(jié)如下:

API 1 - 23

  • 根據(jù)版本新建或用原有的 AM 并把插件 APK 路徑添加到 AM 中:
    • 1-20 : 構(gòu)建新的 AssetManager ,并把宿主 AssetManager 資源路徑添加到新的 AM 中
    • 21-23: 用宿主的 AssetManager.
  • 用新的 AM 構(gòu)建新的 Resources (適配部分廠商).
  • 更新所有 LoadedPlugin 的 Resources.
  • Hook 當(dāng)前進程用新的 Resources 代替.
    • 替換宿主 Context 的 Resources.
    • 替換 ContextImpl.mPackageInfo.mResources.
    • 根據(jù)版本獲取 ResourcesManager 并把新的 Resources 緩存到 ResourcesManager.mActiveResources.
      • 1-18: 反射獲取 ActivityThread.mResourcesManager.
      • 19-23: 創(chuàng)建新的 ResourcesManager 實例.

API 24 - lastest

  • 反射 ContextImpl.mPackageInfo.mSplitResDirs 添加插件 APK 路徑.
  • 根據(jù)版本刷新 ResourcesManager.mResourceImpls 中可加載宿主資源的 ResourcesImpl 對應(yīng)的 ResourcesKey.
    • 24-27: 直接生成新的帶有插件 APk 路徑的 ResoucesKey 并刷新.
    • 28-lastest: 太復(fù)雜了不寫了.
  • 重新創(chuàng)建所有 Resources 實例(包括 Application 和 Activity)對應(yīng)的 ResourcesImpl 為可訪問宿主資源的 ResourcesImpl 并替代 Resources.mResourcesImpl.
  • 更新所有 LoadedPlugin 的 Resources.

參考資料

Android 資源加載機制剖析

Android源碼分析-資源加載機制

聊聊Android中的ContextImpl

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容