Android資源訪問機制

風靈使發表於2018-11-08

Android經常使用getResources()方法獲取app的一些資源,getResource()方法是Context介面的方法,具體是有ContextImpl類實現的,Activity、Service、Application都是繼承自Context介面。

資源獲取的方式是context.getResources,而真正的實現位於ContextImpl中的getResources方法,在ContextImpl中有一個私有成員Resources mResourcesgetResources方法返回的結果就是該物件成員,mResources的賦值則是在ContextImpl的建構函式中:

private ContextImpl(ContextImpl container, ActivityThread mainThread,
      LoadedApk packageInfo, IBinder activityToken, UserHandle user, Boolean restricted,
      Display display, Configuration overrideConfiguration) {
	.......
	  Resources resources = packageInfo.getResources(mainThread);
	if (resources != null) {
		if (activityToken != null
		              || displayId != Display.DEFAULT_DISPLAY
		              || overrideConfiguration != null
		              || (compatInfo != null && compatInfo.applicationScale
		                      != resources.getCompatibilityInfo().applicationScale)) {
			resources = mResourcesManager.getTopLevelResources(packageInfo.getResDir(),
			                  packageInfo.getSplitResDirs(), packageInfo.getOverlayDirs(),
			                  packageInfo.getApplicationInfo().sharedLibraryFiles, displayId,
			                  overrideConfiguration, compatInfo, activityToken);
		}
	}
	mResources = resources;

mPackageInfoLoadedAPK類,所以就是呼叫到LoadedAPKgetResource(ActivityThread)方法中:

public Resources  getResources(ActivityThread mainThread) {
	if (mResources == null) {
		mResources = mainThread.getTopLevelResources(mResDir, mSplitResDirs, mOverlayDirs,
		                 mApplicationInfo.sharedLibraryFiles, Display.DEFAULT_DISPLAY, null, this);
	}
	return mResources;
}

ActivityThread型別的mainThread一個應用中只有一個,函式呼叫如下

Resources getTopLevelResources(String resDir, String[] splitResDirs, String[] overlayDirs,
       String[] libDirs, int displayId, Configuration overrideConfiguration,
       LoadedApk pkgInfo) {
	return mResourcesManager.getTopLevelResources(resDir, splitResDirs, overlayDirs, libDirs,
	           displayId, overrideConfiguration, pkgInfo.getCompatibilityInfo(), null);
}

下面看一下ResourcesManagergetTopLevelResources方法,

public Resources getTopLevelResources(String resDir, String[] splitResDirs,
        String[] overlayDirs, String[] libDirs, 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();
	// resDir can be null if the 'android' package is creating a new Resources object.
	// This is fine, since each AssetManager automatically loads the 'android' package
	// already.
	if (resDir != null) {
		if (assets.addAssetPath(resDir) == 0) {
			return null;
		}
	}
	if (splitResDirs != null) {
		for (String splitResDir : splitResDirs) {
			if (assets.addAssetPath(splitResDir) == 0) {
				return null;
			}
		}
	}
	if (overlayDirs != null) {
		for (String idmapPath : overlayDirs) {
			assets.addOverlayPath(idmapPath);
		}
	}
	if (libDirs != null) {
		for (String libDir : libDirs) {
			if (assets.addAssetPath(libDir) == 0) {
				Slog.w(TAG, "Asset path '" + libDir +
				                        "' does not exist or contains no resources.");
			}
		}
	}
	//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中,所有的資源物件都被儲存在ArrayMap中,首先根據當前的請求引數去查詢資源,如果找到了就返回;否則就建立一個資源物件放到ArrayMap中。為什麼會有多個資源物件,因為res下可能存在多個適配不同裝置、不同解析度、不同系統版本的目錄,按照android系統的設計,不同裝置在訪問同一個應用的時候訪問的資源可以不同,比如drawable-hdpidrawable-xhdpi就是典型的例子。

根據上述程式碼和ResourcesManager採用單例模式,這樣就保證了不同的ContextImpl訪問的是同一套資源,注意,這裡說的同一套資源未必是同一個資源,因為資源可能位於不同的目錄,但它一定是我們的應用的資源。或許這樣來描述更準確,在裝置引數和顯示引數不變的情況下,不同的ContextImpl訪問到的是同一份資源。裝置引數不變是指手機的螢幕和android版本不變,顯示引數不變是指手機的解析度和橫豎屏狀態。也就是說,儘管Application、Activity、Service都有自己的ContextImpl,並且每個ContextImpl都有自己的mResources成員,但是由於它們的mResources成員都來自於唯一的ResourcesManager例項,所以它們看似不同的mResources其實都指向的是同一塊記憶體(C語言的概念),因此,它們的mResources都是同一個物件(在裝置引數和顯示引數不變的情況下)。在橫豎屏切換的情況下且應用中為橫豎屏狀態提供了不同的資源,處在橫屏狀態下的ContextImpl和處在豎屏狀態下的ContextImpl訪問的資源不是同一個資源物件。

public static ResourcesManager getInstance() {
	synchronized (ResourcesManager.class) {
		if (sResourcesManager == null) {
			sResourcesManager = new ResourcesManager();
		}
		return sResourcesManager;
	}
}

Resources物件的建立過程

通過閱讀Resources類的原始碼可以知道,Resources對資源的訪問實際上是通過AssetManager來實現的,那麼如何建立一個Resources物件呢,有人會問,我為什麼要去建立一個Resources物件呢,直接getResources不就可以了嗎?我要說的是在某些特殊情況下你的確需要去建立一個資源物件,比如動態載入apk。很簡單,首先看一下它的幾個構造方法:

public Resources (AssetManager assets, DisplayMetrics metrics, Configuration config) {
	this(assets, metrics, config, CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null);
}

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的底層實現嗎,在ResourcesManagergetTopLevelResources方法中有這麼兩句:

    AssetManager assets = new AssetManager();  
    if (assets.addAssetPath(resDir) == 0) {  
        return null;  
    }  

這兩句就是建立一個AssetManager物件,後面會用這個物件來建立Resources物件,AssetManager就是這麼建立的,assets.addAssetPath(resDir)這句話的意思是把資源目錄裡的資源都載入到AssetManager物件中,具體的實現在jni中,大家感興趣自己去了解下。而資源目錄就是我們的res目錄,當然resDir可以是一個目錄也可以是一個zip檔案。有沒有想過,如果我們把一個未安裝的apk的路徑傳給這個方法,那麼apk中的資源是不是就被載入到AssetManager物件裡面了呢?事實證明,的確是這樣。addAssetPath方法的定義如下,注意到它的註釋裡面有一個{@hide}關鍵字,這意味著即使它只是給Framework自己使用的,因此只能通過反射來呼叫。

public final int addedAssetPath(String path) {
	synchronized (this) {
		int res = addAssetPathNative(path);
		makeStringBlocks(mStringBlocks);
		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());

有了Resources物件,我們就可以通過Resources物件來訪問裡面的各種資源了,通過這種方法,我們可以完成一些特殊的功能,比如換膚、換語言包、動態載入apk等。

另外,除了通過ContextgetResource()方法訪問android應用的資源外,還可以通過PackageManger來獲取。

PackageManager本身只是一個抽象類,具體是由ApplicationPackageManager實現的,獲取了PackageManager以後就可以呼叫PacakageManagergetResourcesForApplication(ApplicationInfo app)來獲取Resources物件了,程式碼如下:

public Resources getResourcesForApplication(
     ApplicationInfo app) throws NameNotFoundException {
	if (app.packageName.equals("system")) {
		return mContext.mMainThread.getSystemContext().getResources();
	}
	final Boolean sameUid = (app.uid == Process.myUid());
	Resources r = mContext.mMainThread.getTopLevelResources(
	             sameUid ? app.sourceDir : app.publicSourceDir,
	             sameUid ? app.splitSourceDirs : app.splitPublicSourceDirs,
	             app.resourceDirs, app.sharedLibraryFiles, Display.DEFAULT_DISPLAY,
	             null, mContext.mPackageInfo);
	if (r != null) {
		return r;
	}
	throw new NameNotFoundException("Unable to open " + app.publicSourceDir);
}

其實PackageManager方式同Context方式最終還是一致的,都是通過mMainThread.getTopLevelResource()方法來獲取Resources的。

相關文章