Android資源訪問機制
Android經常使用getResources()
方法獲取app的一些資源,getResource()
方法是Context
介面的方法,具體是有ContextImpl
類實現的,Activity、Service、Application
都是繼承自Context
介面。
資源獲取的方式是context.getResources
,而真正的實現位於ContextImpl
中的getResources
方法,在ContextImpl
中有一個私有成員Resources mResources
,getResources
方法返回的結果就是該物件成員,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;
而mPackageInfo
是LoadedAPK
類,所以就是呼叫到LoadedAPK
的getResource(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);
}
下面看一下ResourcesManager
的getTopLevelResources
方法,
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-hdpi
和drawable-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
的底層實現嗎,在ResourcesManager
的getTopLevelResources
方法中有這麼兩句:
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等。
另外,除了通過Context
的getResource()
方法訪問android應用的資源外,還可以通過PackageManger
來獲取。
PackageManager
本身只是一個抽象類,具體是由ApplicationPackageManager
實現的,獲取了PackageManager
以後就可以呼叫PacakageManager
的getResourcesForApplication(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
的。
相關文章
- Android 資源載入機制剖析Android
- Sprint資源訪問
- SpringBoot靜態資源訪問Spring Boot
- 02.Android之IPC機制問題Android
- springboot+themeleaf+bootstrap訪問靜態資源/無法訪問靜態資源/圖片Spring Boot
- 加密訪問資源方法總結加密
- 06.Android之訊息機制問題Android
- Springboot中如何訪問靜態資源Spring Boot
- Android Classloader機制Android
- Android NestedScrolling機制Android
- 採坑之Android手機訪問相簿許可權問題Android
- Nginx解決前端訪問資源跨域問題Nginx前端跨域
- [開源] .Net ORM 訪問 Firebird 資料庫ORM資料庫
- SpringMVC下關於靜態資源訪問SpringMVC
- springboot新增靜態資源無法訪問Spring Boot
- CDN及CDN資源訪問流程簡介
- 淺析Windows的訪問許可權檢查機制Windows訪問許可權
- Android IPC 機制分析Android
- Android簽名機制Android
- Android包管理機制Android
- android中反射機制Android反射
- webpack簡單搭建localhost訪問靜態資源Weblocalhost
- Oracle RAC Cache Fusion 系列十三:PCM資源訪問Oracle
- android SAF儲存訪問框架Android框架
- Android Handler機制之Message及Message回收機制Android
- SpringBoot資料訪問之Druid資料來源的使用Spring BootUI
- iOS系統資源排程機制解析iOS
- 理解 Android 訊息機制Android
- Android訊息機制HandlerAndroid
- Android重修課 -- Parcel機制Android
- Android事件分發機制Android事件
- android訊息機制—HandlerAndroid
- Android 之訊息機制Android
- Android Handler機制之ThreadLocalAndroidthread
- Android類裝載機制Android
- Android系統安全機制Android
- 記錄一次二進位制部署k8s時,無法訪問到資源K8S
- Android 訊息機制詳解(Android P)Android