前言
我們知道,在activity內部訪問資源(字符串,圖片等)是很簡單的,只要getResources然後就可以得到Resources對象,有了Resources對象就可以訪問各種資源了,這很簡單,不過本文不是介紹這個的,本文主要介紹在這套邏輯之下的資源加載機制
資源加載機制
很明確,不同的Context得到的都是同一份資源。這是很好理解的,請看下面的分析
得到資源的方式爲context.getResources,而真正的實現位於ContextImpl中的getResources方法,在ContextImpl中有一個成員 private Resources mResources,它就是getResources方法返回的結果,mResources的賦值代碼爲:
mResources = mResourcesManager.getTopLevelResources(mPackageInfo.getResDir(),
Display.DEFAULT_DISPLAY, null, compatInfo, activityToken);
下面看一下ResourcesManager的getTopLevelResources方法,這個方法的思想是這樣的:在ResourcesManager中,所有的資源對象都被存儲在ArrayMap中,首先根據當前的請求參數去查找資源,如果找到了就返回,否則就創建一個資源對象放到ArrayMap中。有一點需要說明的是爲什麼會有多個資源對象,原因很簡單,因爲res下可能存在多個適配不同設備、不同分辨率、不同系統版本的目錄,按照Android系統的設計,不同設備在訪問同一個應用的時候訪問的資源可以不同,比如drawable-hdpi和drawable-xhdpi就是典型的例子。
- public Resources getTopLevelResources(String resDir, int displayId,
- Configuration overrideConfiguration, CompatibilityInfo compatInfo, IBinder token) {
- final float scale = compatInfo.applicationScale;
- ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfiguration, scale,
- token);
- Resources r;
- synchronized (this) {
- // Resources is app scale dependent.
- if (false) {
- Slog.w(TAG, "getTopLevelResources: " + resDir + " / " + scale);
- }
- WeakReference<Resources> wr = mActiveResources.get(key);
- r = wr != null ? wr.get() : null;
- //if (r != null) Slog.i(TAG, "isUpToDate " + resDir + ": " + r.getAssets().isUpToDate());
- if (r != null && r.getAssets().isUpToDate()) {
- if (false) {
- Slog.w(TAG, "Returning cached resources " + r + " " + resDir
- + ": appScale=" + r.getCompatibilityInfo().applicationScale);
- }
- return r;
- }
- }
- //if (r != null) {
- // Slog.w(TAG, "Throwing away out-of-date resources!!!! "
- // + r + " " + resDir);
- //}
- AssetManager assets = new AssetManager();
- if (assets.addAssetPath(resDir) == 0) {
- return null;
- }
- //Slog.i(TAG, "Resource: key=" + key + ", display metrics=" + metrics);
- DisplayMetrics dm = getDisplayMetricsLocked(displayId);
- Configuration config;
- boolean isDefaultDisplay = (displayId == Display.DEFAULT_DISPLAY);
- final boolean hasOverrideConfig = key.hasOverrideConfiguration();
- if (!isDefaultDisplay || hasOverrideConfig) {
- config = new Configuration(getConfiguration());
- if (!isDefaultDisplay) {
- applyNonDefaultDisplayMetricsToConfigurationLocked(dm, config);
- }
- if (hasOverrideConfig) {
- config.updateFrom(key.mOverrideConfiguration);
- }
- } else {
- config = getConfiguration();
- }
- r = new Resources(assets, dm, config, compatInfo, token);
- if (false) {
- Slog.i(TAG, "Created app resources " + resDir + " " + r + ": "
- + r.getConfiguration() + " appScale="
- + r.getCompatibilityInfo().applicationScale);
- }
- synchronized (this) {
- WeakReference<Resources> wr = mActiveResources.get(key);
- Resources existing = wr != null ? wr.get() : null;
- if (existing != null && existing.getAssets().isUpToDate()) {
- // Someone else already created the resources while we were
- // unlocked; go ahead and use theirs.
- r.getAssets().close();
- return existing;
- }
- // XXX need to remove entries when weak references go away
- mActiveResources.put(key, new WeakReference<Resources>(r));
- return r;
- }
- }
代碼:單例模式的ResourcesManager類
- public static ResourcesManager getInstance() {
- synchronized (ResourcesManager.class) {
- if (sResourcesManager == null) {
- sResourcesManager = new ResourcesManager();
- }
- return sResourcesManager;
- }
- }
Resources對象的創建過程
通過閱讀Resources類的源碼可以知道,Resources對資源的訪問實際上是通過AssetManager來實現的,那麼如何創建一個Resources對象呢,有人會問,我爲什麼要去創建一個Resources對象呢,直接getResources不就可以了嗎?我要說的是在某些特殊情況下你的確需要去創建一個資源對象,比如動態加載apk。很簡單,首先看一下它的幾個構造方法:
- /**
- * Create a new Resources object on top of an existing set of assets in an
- * AssetManager.
- *
- * @param assets Previously created AssetManager.
- * @param metrics Current display metrics to consider when
- * selecting/computing resource values.
- * @param config Desired device configuration to consider when
- * selecting/computing resource values (optional).
- */
- public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
- this(assets, metrics, config, CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null);
- }
- /**
- * Creates a new Resources object with CompatibilityInfo.
- *
- * @param assets Previously created AssetManager.
- * @param metrics Current display metrics to consider when
- * selecting/computing resource values.
- * @param config Desired device configuration to consider when
- * selecting/computing resource values (optional).
- * @param compatInfo this resource's compatibility info. Must not be null.
- * @param token The Activity token for determining stack affiliation. Usually null.
- * @hide
- */
- public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config,
- CompatibilityInfo compatInfo, IBinder token) {
- mAssets = assets;
- mMetrics.setToDefaults();
- if (compatInfo != null) {
- mCompatibilityInfo = compatInfo;
- }
- mToken = new WeakReference<IBinder>(token);
- updateConfiguration(config, metrics);
- assets.ensureStringBlocks();
- }
public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config)
它接受3個參數,第一個是AssetManager,後面兩個是和設備相關的配置參數,我們可以直接用當前應用的配置就好,所以,問題的關鍵在於如何創建AssetManager,下面請看分析,爲了創建一個我們自己的AssetManager,我們先去看看系統是怎麼創建的。還記得getResources的底層實現嗎,在ResourcesManager的getTopLevelResources方法中有這麼兩句:
- AssetManager assets = new AssetManager();
- if (assets.addAssetPath(resDir) == 0) {
- return null;
- }
這兩句就是創建一個AssetManager對象,後面會用這個對象來創建Resources對象,ok,AssetManager就是這麼創建的,assets.addAssetPath(resDir)這句話的意思是把資源目錄裏的資源都加載到AssetManager對象中,具體的實現在jni中,大家感興趣自己去了解下。而資源目錄就是我們的res目錄,當然resDir可以是一個目錄也可以是一個zip文件。有沒有想過,如果我們把一個未安裝的apk的路徑傳給這個方法,那麼apk中的資源是不是就被加載到AssetManager對象裏面了呢?事實證明,的確是這樣,具體情況可以參見Android apk動態加載機制的研究(二):資源加載和activity生命週期管理這篇文章。addAssetPath方法的定義如下,注意到它的註釋裏面有一個{@hide}關鍵字,這意味着即使它是public的,但是外界仍然無法訪問它,因爲android sdk導出的時候會自動忽略隱藏的api,因此只能通過反射來調用。
- /**
- * 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) {
- int res = addAssetPathNative(path);
- return res;
- }
有了AssetManager對象後,我們就可以創建自己的Resources對象了,代碼如下:
- try {
- AssetManager assetManager = AssetManager.class.newInstance();
- Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
- addAssetPath.invoke(assetManager, mDexPath);
- mAssetManager = assetManager;
- } catch (Exception e) {
- e.printStackTrace();
- }
- Resources currentRes = this.getResources();
- mResources = new Resources(mAssetManager, currentRes.getDisplayMetrics(),
- currentRes.getConfiguration());