我們知道在開發中,需要應用程序資源,如應用工程中assets和res目錄下的圖片,layout,values等,或者需要系統內置的資源。我們獲取這些資源的入口對象都是Resources對象,並博文將分析如何獲取Resources對象。
獲取Resources的過程:
(1)將framework/framework-res.apk和應用資源apk裝載爲Resources對象。
(2)獲取Resources對象
獲取Resources對象有兩種方式,第一種通過Context,第二種通過PackageManager。
1. 通過Context獲取Resources對象
在一個Acitvity或者一個Service中,我們直接this.getResources()方法,就可以獲得Reousrces對象。其實Acitivity或者Service本質上就是一個Context.getResources()方法來自Context,而真正實現Context接口是ContextImpl類,所以調用的實際上時ContextImpl類的getResources()方法。
我們查看ContextImpl類源碼可以看到,getResources方法直接返回內部的mResources變量,而對該變量的賦值在私有的構造方法中。
core/java/android/app/ContextImpl.java
private ContextImpl(ContextImpl container, ActivityThread mainThread,
LoadedApk packageInfo, IBinder activityToken, UserHandle user, boolean restricted,
Display display, Configuration overrideConfiguration) {
mOuterContext = this;
mMainThread = mainThread;
mActivityToken = activityToken;
mRestricted = restricted;
if (user == null) {
user = Process.myUserHandle();
}
mUser = user;
mPackageInfo = packageInfo;
mContentResolver = new ApplicationContentResolver(this, mainThread, user);
mResourcesManager = ResourcesManager.getInstance();
mDisplay = display;
mOverrideConfiguration = overrideConfiguration;
final int displayId = getDisplayId();
CompatibilityInfo compatInfo = null;
if (container != null) {
compatInfo = container.getDisplayAdjustments(displayId).getCompatibilityInfo();
}
if (compatInfo == null && displayId == Display.DEFAULT_DISPLAY) {
compatInfo = packageInfo.getCompatibilityInfo();
}
mDisplayAdjustments.setCompatibilityInfo(compatInfo);
mDisplayAdjustments.setActivityToken(activityToken);
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(), displayId,
overrideConfiguration, compatInfo, activityToken);
}
}
mResources = resources;
if (container != null) {
mBasePackageName = container.mBasePackageName;
mOpPackageName = container.mOpPackageName;
} else {
mBasePackageName = packageInfo.mPackageName;
ApplicationInfo ainfo = packageInfo.getApplicationInfo();
if (ainfo.uid == Process.SYSTEM_UID && ainfo.uid != Process.myUid()) {
// Special case: system components allow themselves to be loaded in to other
// processes. For purposes of app ops, we must then consider the context as
// belonging to the package of this process, not the system itself, otherwise
// the package+uid verifications in app ops will fail.
mOpPackageName = ActivityThread.currentPackageName();
} else {
mOpPackageName = mBasePackageName;
}
}
}
mResources又是調用LoadedApk的getResources方法進行賦值。代碼如下。
public Resources getResources(ActivityThread mainThread) {
if (mResources == null) {
mResources = mainThread.getTopLevelResources(mResDir, this);
}
return mResources;
}
從代碼中可以看到,最終mResources的賦值是由AcitivtyThread的getTopLevelResources方法返回。代碼如下。
Resources getTopLevelResources(String resDir, CompatibilityInfo compInfo) {
ResourcesKey key = new ResourcesKey(resDir, compInfo.applicationScale);
Resources r;
synchronized (mPackages) {
// Resources is app scale dependent.
if (false) {
Slog.w(TAG, "getTopLevelResources: " + resDir + " / "
+ compInfo.applicationScale);
}
WeakReference<Resources> wr = mActiveResources.get(key);
r = wr != null ? wr.get() : null;
if (r != null && r.getAssets().isUpToDate()) {
if (false) {
Slog.w(TAG, "Returning cached resources " + r + " " + resDir
+ ": appScale=" + r.getCompatibilityInfo().applicationScale);
}
return r;
}
}
AssetManager assets = new AssetManager();
if (assets.addAssetPath(resDir) == 0) {
return null;
}
DisplayMetrics metrics = getDisplayMetricsLocked(false);
r = new Resources(assets, metrics, getConfiguration(), compInfo);
if (false) {
Slog.i(TAG, "Created app resources " + resDir + " " + r + ": "
+ r.getConfiguration() + " appScale="
+ r.getCompatibilityInfo().applicationScale);
}
synchronized (mPackages) {
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;
}
}
以上代碼中,mActiveResources對象內部保存了該應用程序所使用到的所有Resources對象,其類型爲Hash<ResourcesKey,WeakReference<Resourcces>>,可以看出這些Resources對象都是以一個弱引用的方式保存,以便在內存緊張時可以釋放Resources所佔內存。
ResourcesKey的構造需要resDir和compInfo.applicationScale。resdDir變量的含義是資源文件所在路徑,實際指的是APK程序所在路徑,比如可以是:/data/app/com.haii.android.xxx-1.apk,該apk會對應/data/dalvik-cache目錄下的:data@[email protected]@classes.dex文件。
所以,如果一個應用程序沒有訪問該程序以外的資源,那麼mActiveResources變量中就僅有一個Resources對象。這也從側面說明,mActiveResources內部可能包含多個Resources對象,條件是必須有不同的ResourceKey,也就是必須有不同的resDir,這就意味着一個應用程序可以訪問另外的APK文件,並從中讀取讀取其資源。(PS:其實目前的“換膚”就是採用加載不同的資源apk實現主題切換的)
如果mActivityResources對象中沒有包含所要的Resources對象,那麼,就重新建立一個Resources對象
r = new Resources(assets, metrics, getConfiguration(), compInfo);
可以看出構造一個Resources需要一個AssetManager對象,一個DisplayMetrics對象,一個Configuration對象,一個CompatibilityInfo對象,後三者傳入的對象都與設備或者Android平臺相關的參數,因爲資源的使用與這些信息總是相關。還有一個AssetManager對象,其實它並不是訪問項目中res/assets下的資源,而是訪問res下所有的資源。以上代碼中的addAssetPath(resDir)非常關鍵,它爲所創建的AssetManager對象添加一個資源路徑。
AssetManager類的構造函數如下:
public AssetManager() {
synchronized (this) {
if (DEBUG_REFS) {
mNumRefs = 0;
incRefsLocked(this.hashCode());
}
init();
if (localLOGV) Log.v(TAG, "New asset manager: " + this);
ensureSystemAssets();
}
}
構造方法中調用兩個方法init()和ensureSystemAssets(),init方法是一個native實現。AssetManager.java對應的C++文件是android_util_AssetManager.cpp(注意不是AssetManager.cpp,它是C++層內部使用的cpp文件,與Java層無關)。下面看一下init()的native實現。
static void android_content_AssetManager_init(JNIEnv* env, jobject clazz)
{
AssetManager* am = new AssetManager();
if (am == NULL) {
jniThrowException(env, "java/lang/OutOfMemoryError", "");
return;
}
am->addDefaultAssets();
LOGV("Created AssetManager %p for Java object %p\n", am, clazz);
env->SetIntField(clazz, gAssetManagerOffsets.mObject, (jint)am);
}
首先創建一個C++類的AssetManager對象,然後調用am->addDefaultAssets()方法,該方法的作用就是把framework的資源文件添加到這個AssetManager對象的路徑中。最後調用setInitField()方法把C++創建的AssetManager對象的引用保存到Java端的mObject變量中,這種方式是常用的C++層與Java層通信的方式。
addDefaultAssets代碼如下:
bool AssetManager::addDefaultAssets()
{
const char* root = getenv("ANDROID_ROOT");
LOG_ALWAYS_FATAL_IF(root == NULL, "ANDROID_ROOT not set");
String8 path(root);
path.appendPath(kSystemAssets);
return addAssetPath(path, NULL);
}
該函數首先獲取Android的根目錄,getenv是一個Linux系統調用,用戶同樣可以使用以下終端命令獲取該值。
獲得根目錄後,再與kSystemAssets路徑進行組合,該變量的定義如下:
static const char* kSystemAssets = "framework/framework-res.apk";
所以最終獲得的路徑文件名稱爲/system/framework/framework-res.apk,這正式framework對應的資源文件。
分析完了AssetManager的init方法,再來看一下ensureSystemAssets方法。
private static void ensureSystemAssets() {
synchronized (sSync) {
if (sSystem == null) {
AssetManager system = new AssetManager(true);
system.makeStringBlocks(false);
sSystem = system;
}
}
}
該方法實際上僅在framework啓動時就已經調用了,因爲sSystem是一個靜態的AssetManager對象,該變量在Zygote啓動時已經賦值了,以後都不爲空,所以該方法形同虛設。
由此可以知道,Resources對象內部的AssetManager對象除了包含應用程序本身的資源路徑外,還包含了framework的資源路徑,這就是爲什麼應用程序僅使用Resources對象就可以訪問應用資源和系統資源的原因。如
Resources res = getResources();
Drawable btnPic = res.getDrawable(android.R.drawable.btn_default_small);
那麼如何AssetManager如何區分訪問的是系統資源還是應用資源呢?當使用getXXX(int id)訪問資源時,如果id值小於0x10000000時,AssetManager會認爲要訪問的是系統資源。因爲aapt在對系統資源進行編譯時,所有的資源id都被編譯爲小於該值的一個int值,而當訪問應用資源時,id值都大於0x70000000。
創建好了Resources對象後,就把該變量緩存到mActiveResources中,以便以後繼續使用。
訪問Resources內部的整個流程如下圖。
2. 通過PackageManager獲取Resources對象
文件路徑/frameworks/base/+/android-4.4.4_r2.0.1/core/java/android/content/pm/PackageManager.java
packageManager爲抽象類,,跟Resource相關的方法有:
public abstract Resources getResourcesForActivity(ComponentName activityName)
throws NameNotFoundException;
public abstract Resources getResourcesForApplication(ApplicationInfo app)
public abstract Resources getResourcesForApplication(String appPackageName)
throws NameNotFoundException;
/** @hide */
public abstract Resources getResourcesForApplicationAsUser(String appPackageName, int userId)
throws NameNotFoundException;
throws NameNotFoundException;
具體的實現類爲ApplicationPackageManager.java
@Override public Resources getResourcesForActivity(
ComponentName activityName) throws NameNotFoundException {
return getResourcesForApplication(
getActivityInfo(activityName, 0).applicationInfo);
}
@Override public Resources getResourcesForApplication(
ApplicationInfo app) throws NameNotFoundException {
if (app.packageName.equals("system")) {
return mContext.mMainThread.getSystemContext().getResources();
}
Resources r = mContext.mMainThread.getTopLevelResources(
app.uid == Process.myUid() ? app.sourceDir : app.publicSourceDir,
Display.DEFAULT_DISPLAY, null, mContext.mPackageInfo);
if (r != null) {
return r;
}
throw new NameNotFoundException("Unable to open " + app.publicSourceDir);
}
@Override public Resources getResourcesForApplication(
String appPackageName) throws NameNotFoundException {
return getResourcesForApplication(
getApplicationInfo(appPackageName, 0));
}
/** @hide */
@Override
public Resources getResourcesForApplicationAsUser(String appPackageName, int userId)
throws NameNotFoundException {
if (userId < 0) {
throw new IllegalArgumentException(
"Call does not support special user #" + userId);
}
if ("system".equals(appPackageName)) {
return mContext.mMainThread.getSystemContext().getResources();
}
try {
ApplicationInfo ai = mPM.getApplicationInfo(appPackageName, 0, userId);
if (ai != null) {
return getResourcesForApplication(ai);
}
} catch (RemoteException e) {
throw new RuntimeException("Package manager has died", e);
}
throw new NameNotFoundException("Package " + appPackageName + " doesn't exist");
}
1.包名爲system的情況,mContext.mMainThread.getSystemContext().getResources()
ActivityThread.getSystemContext
public ContextImpl getSystemContext() {
synchronized (this) {
if (mSystemContext == null) {
mSystemContext = ContextImpl.createSystemContext(this);
}
return mSystemContext;
}
}
ContextImpl.createSystemContext
static ContextImpl createSystemContext(ActivityThread mainThread) {
LoadedApk packageInfo = new LoadedApk(mainThread);
ContextImpl context = new ContextImpl(null, mainThread,
packageInfo, null, null, false, null, null);
context.mResources.updateConfiguration(context.mResourcesManager.getConfiguration(),
context.mResourcesManager.getDisplayMetricsLocked(Display.DEFAULT_DISPLAY));
return context;
}
static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo) {
if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
return new ContextImpl(null, mainThread,
packageInfo, null, null, false, null, null);
}
static ContextImpl createActivityContext(ActivityThread mainThread,
LoadedApk packageInfo, IBinder activityToken) {
if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
if (activityToken == null) throw new IllegalArgumentException("activityInfo");
return new ContextImpl(null, mainThread,
packageInfo, activityToken, null, false, null, null);
}
private ContextImpl(ContextImpl container, ActivityThread mainThread,
LoadedApk packageInfo, IBinder activityToken, UserHandle user, boolean restricted,
Display display, Configuration overrideConfiguration) {
mOuterContext = this;
mMainThread = mainThread;
mActivityToken = activityToken;
mRestricted = restricted;
if (user == null) {
user = Process.myUserHandle();
}
mUser = user;
mPackageInfo = packageInfo;
mContentResolver = new ApplicationContentResolver(this, mainThread, user);
mResourcesManager = ResourcesManager.getInstance();
mDisplay = display;
mOverrideConfiguration = overrideConfiguration;
final int displayId = getDisplayId();
CompatibilityInfo compatInfo = null;
if (container != null) {
compatInfo = container.getDisplayAdjustments(displayId).getCompatibilityInfo();
}
if (compatInfo == null && displayId == Display.DEFAULT_DISPLAY) {
compatInfo = packageInfo.getCompatibilityInfo();
}
mDisplayAdjustments.setCompatibilityInfo(compatInfo);
mDisplayAdjustments.setActivityToken(activityToken);
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(), displayId,
overrideConfiguration, compatInfo, activityToken);
}
}
mResources = resources;
if (container != null) {
mBasePackageName = container.mBasePackageName;
mOpPackageName = container.mOpPackageName;
} else {
mBasePackageName = packageInfo.mPackageName;
ApplicationInfo ainfo = packageInfo.getApplicationInfo();
if (ainfo.uid == Process.SYSTEM_UID && ainfo.uid != Process.myUid()) {
// Special case: system components allow themselves to be loaded in to other
// processes. For purposes of app ops, we must then consider the context as
// belonging to the package of this process, not the system itself, otherwise
// the package+uid verifications in app ops will fail.
mOpPackageName = ActivityThread.currentPackageName();
} else {
mOpPackageName = mBasePackageName;
}
}
}
最終的mResource是通過packageinfo.getResources和mResoucesManager.getTopLevelResources得到
packageInfo爲core/java/android/app/LoadedApk.java
public Resources getResources(ActivityThread mainThread) {
if (mResources == null) {
mResources = mainThread.getTopLevelResources(mResDir,
Display.DEFAULT_DISPLAY, null, this);
}
return mResources;
}
core/java/android/app/ResourcesManager.java
/**
* Creates the top level Resources for applications with the given compatibility info.
*
* @param resDir the resource directory.
* @param compatInfo the compability info. Must not be null.
* @param token the application token for determining stack bounds.
*/
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;
}
}
這裏我們可以看到創建的Resouces保存到ResourcesManager中的mActiveResources中,相同的包名存對應的key,存在對應的Resources
final ArrayMap<ResourcesKey, WeakReference<Resources> > mActiveResources
= new ArrayMap<ResourcesKey, WeakReference<Resources> >();
2.通用情況,mContext.mMainThread.getTopLevelResources
<pre name="code" class="java"> /**
* Creates the top level resources for the given package.
*/
Resources getTopLevelResources(String resDir,
int displayId, Configuration overrideConfiguration,
LoadedApk pkgInfo) {
return mResourcesManager.getTopLevelResources(resDir, displayId, overrideConfiguration,
pkgInfo.getCompatibilityInfo(), null);
}
結果同樣通過ResourcesManager.getTopLevelResources來獲取Resources
結論:
1.獲取Resources,最終都是通過ResourcesManager獲取
2.ResourcesManager中使用
ArrayMap<ResourcesKey, WeakReference<Resources> > mActiveResources保存Resources的鍵值對
在內存不足是,弱引用會被回收
PS:
在Resources被回收後,會重新創建Resources對象
->Activity.getResources
->ContextThemeWrapper.getResources
->ContextWrapper.createConfigurationContext
->ContextImpl.createConfigurationContext
ContextThemeWrapper.getResources
public Resources getResources() {
if (mResources != null) {
return mResources;
}
if (mOverrideConfiguration == null) {
mResources = super.getResources();
return mResources;
} else {
Context resc = createConfigurationContext(mOverrideConfiguration);
mResources = resc.getResources();
return mResources;
}
}
ContextWrapper.createConfigurationContext
public Context createConfigurationContext(Configuration overrideConfiguration) {
return mBase.createConfigurationContext(overrideConfiguration);
}
ContextImpl.createConfigurationContext
public Context createConfigurationContext(Configuration overrideConfiguration) {
if (overrideConfiguration == null) {
throw new IllegalArgumentException("overrideConfiguration must not be null");
}
return new ContextImpl(this, mMainThread, mPackageInfo, mActivityToken,
mUser, mRestricted, mDisplay, overrideConfiguration);
}
最後調用ContextImpl的構造方法創建Context
良心的公衆號,更多精品文章,不要忘記關注哈
《Android和Java技術驛站》