轉老羅 Android應用程式資源的查詢過程分析
原文地址 http://blog.csdn.net/luoshengyang/article/details/8806798 轉載請說明
我們知道,在Android系統中,每一個應用程式一般都會配置很多資源,用來適配不同密度、大小和方向的螢幕,以及適配不同的國家、地區和語言等等。這些資源是在應用程式執行時自動根據裝置的當前配置資訊進行適配的。這也就是說,給定一個相同的資源ID,在不同的裝置配置之下,查詢到的可能是不同的資源。這個資源查詢過程對應用程式來說,是完全透明的。在本文中,我們就詳細分析資源管理框架是如何根據ID來查詢資源的。
從前面Android應用程式資源管理器(Asset Manager)的建立過程分析一文可以知道,Android資源管理框架實際就是由AssetManager和Resources兩個類來實現的。其中,Resources類可以根據ID來查詢資源,而AssetManager類根據檔名來查詢資源。事實上,如果一個資源ID對應的是一個檔案,那麼Resources類是先根據ID來找到資原始檔名稱,然後再將該檔名稱交給AssetManager類來開啟對應的檔案的,這個過程如圖1所示。
圖1 應用程式查詢資源的過程示意圖
在圖1中,Resources類根據資源ID來查到資源名稱實際上也是要通過AssetManager類來實現的,這是因為資源ID與資源名稱的對應關係是由打包在APK裡面的resources.arsc檔案中的。當Resources類查詢的資源對應的是一個檔案的時候,它就會再次將資源名稱交給AssetManager,以便後者可以開啟對應的檔案,否則的話,上一步找到的資源名稱就是最終的查詢結果。
從前面Android資源管理框架(Asset Manager)簡要介紹和學習計劃一文又可以知道,Android應用程式資源是可以劃分是很多類別的,但是從資源查詢的過程來看,它們可以歸結為兩大類。第一類資源是不對應有檔案的,而第二類資源是對應有檔案的,例如,字串資源是直接編譯在resources.arsc檔案中的,而介面佈局資源是在APK包裡面是對應的單獨的檔案的。如上所述,不對應檔案的資源只需要執行從資源ID到資源名稱的轉換即可,而對應有檔案的資源還需要根據資源名稱來開啟對應的檔案。在本文中,我們就以介面佈局資源的查詢過程為例,來說明Android資源管理框架查詢資源的過程。
我們知道,每一個Activity元件建立的時候,它的成員函式onCreate都會被呼叫,而在Activity元件的成員函式onCreate中,我們基本上都無一例外地呼叫setContentView來設定Activity元件的介面。在呼叫Activity元件的成員函式setContentView的時候,需要指定一個layout型別的資源ID,以便Android資源管理框架可以找到指定的Xml資原始檔來填充(inflate)為Activity元件的介面。接下來,我們就從Activity類的成員函式setContentView開始,分析Android資源管理框架查詢layout資源的過程,如圖2所示。
圖2 型別為layout的資源的查詢過程
這個過程可以分為22個步驟,接下來我們就詳細分析每一個步驟。
Step 1. Activity.setContentView
[java] view plain copy print ?- public class Activity extends ContextThemeWrapper
- implements LayoutInflater.Factory,
- Window.Callback, KeyEvent.Callback,
- OnCreateContextMenuListener, ComponentCallbacks {
- ......
- private Window mWindow;
- ......
- public Window getWindow() {
- return mWindow;
- }
- .....
- public void setContentView(int layoutResID) {
- getWindow().setContentView(layoutResID);
- }
- ......
- }
public class Activity extends ContextThemeWrapper
implements LayoutInflater.Factory,
Window.Callback, KeyEvent.Callback,
OnCreateContextMenuListener, ComponentCallbacks {
......
private Window mWindow;
......
public Window getWindow() {
return mWindow;
}
.....
public void setContentView(int layoutResID) {
getWindow().setContentView(layoutResID);
}
......
}
這個函式定義在檔案frameworks/base/core/java/android/app/Activity.java中。
從前面Android應用程式視窗(Activity)的視窗物件(Window)的建立過程分析一文可以知道,Activity類的成員變數mWindow指向的是一個PhoneWindow物件,因此,Activity類的成員函式setContentView實際上是呼叫PhoneWindow類的成員函式setContentView來進一步操作。
Step 2. PhoneWindow.setContentView
[java] view plain copy print ?- public class PhoneWindow extends Window implements MenuBuilder.Callback {
- ......
- // This is the view in which the window contents are placed. It is either
- // mDecor itself, or a child of mDecor where the contents go.
- private ViewGroup mContentParent;
- ......
- private LayoutInflater mLayoutInflater;
- ......
- @Override
- public void setContentView(int layoutResID) {
- if (mContentParent == null) {
- installDecor();
- } else {
- mContentParent.removeAllViews();
- }
- mLayoutInflater.inflate(layoutResID, mContentParent);
- final Callback cb = getCallback();
- if (cb != null) {
- cb.onContentChanged();
- }
- }
- ......
- }
public class PhoneWindow extends Window implements MenuBuilder.Callback {
......
// This is the view in which the window contents are placed. It is either
// mDecor itself, or a child of mDecor where the contents go.
private ViewGroup mContentParent;
......
private LayoutInflater mLayoutInflater;
......
@Override
public void setContentView(int layoutResID) {
if (mContentParent == null) {
installDecor();
} else {
mContentParent.removeAllViews();
}
mLayoutInflater.inflate(layoutResID, mContentParent);
final Callback cb = getCallback();
if (cb != null) {
cb.onContentChanged();
}
}
......
}
這個函式定義在檔案frameworks/base/policy/src/com/android/internal/policy/impl/PhoneWindow.java中。
PhoneWindow類的成員變數mContentParent用來描述一個型別為DecorView的檢視物件,或者這個型別為DecorView的檢視物件的一個子檢視物件,用作UI容器。當它的值等於null的時候,就說明當前正在處理的Activity元件的檢視物件還沒有建立。在這種情況下,就會呼叫成員函式installDecor來建立當前正在處理的Activity元件的檢視物件。否則的話,就說明是要重新設定當前正在處理的Activity元件的檢視。在重新設定之前,首先呼叫成員變數mContentParent所描述的一個ViewGroup物件來移除原來的UI內容。
PhoneWindow類的成員變數mLayoutInflater指向的是一個PhoneLayoutInflater物件。PhoneLayoutInflater類是從LayoutInflater類繼續下來的,同時它也繼承了LayoutInflater類的成員函式inflate。通過呼叫PhoneWindow類的成員變數mLayoutInflater所指向的一個PhoneLayoutInflater物件的成員函式inflate,也就是從父類繼承下來的成員函式inflate,就可以將引數layoutResID所描述的一個UI佈局設定到mContentParent所描述的一個檢視容器中去。這樣就可以將當前正在處理的Activity元件的UI創建出來。
最後,PhoneWindow類的成員函式還會呼叫一個Callback介面的成員函式onContentChanged來通知當前正在處理的Activity元件,它的檢視內容發生改變了。從前面Android應用程式視窗(Activity)的視窗物件(Window)的建立過程分析一文可以知道,每一個Activity元件都實現了一個Callback介面,並且將這個Callback介面設定到了與它所關聯的PhoneWindow的內部去,因此,最後呼叫的實際上是Activity類的成員函式onContentChanged。
接下來,我們就繼續分析LayoutInflater類的成員函式inflate的實現,以便可以瞭解Android資源管理框架是如何找到引數layoutResID所描述的UI佈局檔案的。
Step 3. LayoutInflater.inflate
[java] view plain copy print ?- public abstract class LayoutInflater {
- ......
- public View inflate(int resource, ViewGroup root) {
- return inflate(resource, root, root != null);
- }
- ......
- public View inflate(int resource, ViewGroup root, boolean attachToRoot) {
- ......
- XmlResourceParser parser = getContext().getResources().getLayout(resource);
- try {
- return inflate(parser, root, attachToRoot);
- } finally {
- parser.close();
- }
- }
- ......
- }
public abstract class LayoutInflater {
......
public View inflate(int resource, ViewGroup root) {
return inflate(resource, root, root != null);
}
......
public View inflate(int resource, ViewGroup root, boolean attachToRoot) {
......
XmlResourceParser parser = getContext().getResources().getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
......
}
這個函式定義在檔案frameworks/base/core/java/android/view/LayoutInflater.java中。
LayoutInflater類兩個引數版本的成員函式inflate通過呼叫三個引數版本的成員函式inflate來查詢引數resource所描述的UI佈局檔案。
在LayoutInflater類三個引數版本的成員函式inflate中,首先是獲得用來描述當前執行上下文環境的一個Resources物件,然後接呼叫這個Resources物件的成員函式getLayout來查詢引數resource所描述的UI佈局檔案。
Resources類的成員函式getLayout找到了指定的UI佈局檔案之後,就會開啟它。由於Android系統的UI佈局檔案是一個Xml檔案,因此,Resources類的成員函式getLayout開啟它之後,得到的是一個XmlResourceParser物件。有了這個XmlResourceParser物件之後,LayoutInflater類三個引數版本的成員函式inflate就將它傳遞給另外一個三個引數版本的成員函式inflate,以便後者可以通過它來建立一個UI介面。
接下來,我們就首先分析Resources類的成員函式getLayout的實現,然後再分析LayoutInflater類的另外一個三個引數版本的成員函式inflate的實現。
Step 4. Resources.getLayout
[java] view plain copy print ?- public class Resources {
- ......
- public XmlResourceParser getLayout(int id) throws NotFoundException {
- return loadXmlResourceParser(id, "layout");
- }
- ......
- }
public class Resources {
......
public XmlResourceParser getLayout(int id) throws NotFoundException {
return loadXmlResourceParser(id, "layout");
}
......
}
這個函式定義在檔案frameworks/base/core/java/android/content/res/Resources.java中。
Resources類的成員函式getLayout的實現很簡單,它通過呼叫另外一個成員函式loadXmlResourceParser來查詢並且開啟由引數id所描述的一個UI佈局檔案。
Step 5. Resources.loadXmlResourceParser
[java] view plain copy print ?- public class Resources {
- ......
- /*package*/ XmlResourceParser loadXmlResourceParser(int id, String type)
- throws NotFoundException {
- synchronized (mTmpValue) {
- TypedValue value = mTmpValue;
- getValue(id, value, true);
- if (value.type == TypedValue.TYPE_STRING) {
- return loadXmlResourceParser(value.string.toString(), id,
- value.assetCookie, type);
- }
- throw new NotFoundException(
- "Resource ID #0x" + Integer.toHexString(id) + " type #0x"
- + Integer.toHexString(value.type) + " is not valid");
- }
- }
- ......
- }
public class Resources {
......
/*package*/ XmlResourceParser loadXmlResourceParser(int id, String type)
throws NotFoundException {
synchronized (mTmpValue) {
TypedValue value = mTmpValue;
getValue(id, value, true);
if (value.type == TypedValue.TYPE_STRING) {
return loadXmlResourceParser(value.string.toString(), id,
value.assetCookie, type);
}
throw new NotFoundException(
"Resource ID #0x" + Integer.toHexString(id) + " type #0x"
+ Integer.toHexString(value.type) + " is not valid");
}
}
......
}
這個函式定義在檔案frameworks/base/core/java/android/content/res/Resources.java中。
引數id描述的是一個資源ID,Resources類的成員函式loadXmlResourceParser首先呼叫另外一個成員函式getValue來獲得該資源ID所對應的資源值,並且儲存在一個型別為TypedValue的變數value中。在我們這個情景中,引數id描述的是一個型別為layout的資源ID,從前面Android應用程式資源的編譯和打包過程分析一文可以知道,型別為layout的資源ID對應的資源值即為一個UI佈局檔名稱。有了這個UI佈局檔名稱之後,Resources類的成員函式loadXmlResourceParser接著再呼叫另外一個四個引數版本的成員函式loadXmlResourceParser來載入對應的UI佈局檔案,並且得到一個XmlResourceParser物件返回給呼叫者。
注意,如果Resources類的成員函式getValue沒有找到與引數id所描述的資源,或者找到的資源的值不是字串型別的,那麼Resources類的成員函式loadXmlResourceParser就會丟擲一個型別為NotFoundException的異常。
接下來,我們就首先分析Resources類的成員函式getValue的實現,接著再分析Resources類四個引數版本的成員函式loadXmlResourceParser的實現。
Step 6. Resources.getValue
[java] view plain copy print ?- public class Resources {
- ......
- /*package*/ final AssetManager mAssets;
- ......
- public void getValue(int id, TypedValue outValue, boolean resolveRefs)
- throws NotFoundException {
- boolean found = mAssets.getResourceValue(id, outValue, resolveRefs);
- if (found) {
- return;
- }
- throw new NotFoundException("Resource ID #0x"
- + Integer.toHexString(id));
- }
- ......
- }
public class Resources {
......
/*package*/ final AssetManager mAssets;
......
public void getValue(int id, TypedValue outValue, boolean resolveRefs)
throws NotFoundException {
boolean found = mAssets.getResourceValue(id, outValue, resolveRefs);
if (found) {
return;
}
throw new NotFoundException("Resource ID #0x"
+ Integer.toHexString(id));
}
......
}
這個函式定義在檔案frameworks/base/core/java/android/content/res/Resources.java中。
Resources類的成員變數mAssets指向的是一個AssetManager物件,Resources類的成員函式getValue通過呼叫它的成員函式getResourceValue來獲得與引數id所對應的資源的值。注意,如果AssetManager類的成員函式getResourceValue查詢不到與引數id所對應的資源,那麼Resources類的成員函式getValue就會丟擲一個型別為NotFoundException的異常。
接下來,我們就繼續分析AssetManager類的成員函式getResourceValue的實現。
Step 7. AssetManager.getResourceValue
[java] view plain copy print ?- public final class AssetManager {
- ......
- private StringBlock mStringBlocks[] = null;
- ......
- /*package*/ final boolean getResourceValue(int ident,
- TypedValue outValue,
- boolean resolveRefs)
- {
- int block = loadResourceValue(ident, outValue, resolveRefs);
- if (block >= 0) {
- if (outValue.type != TypedValue.TYPE_STRING) {
- return true;
- }
- outValue.string = mStringBlocks[block].get(outValue.data);
- return true;
- }
- return false;
- }
- ......
- }
public final class AssetManager {
......
private StringBlock mStringBlocks[] = null;
......
/*package*/ final boolean getResourceValue(int ident,
TypedValue outValue,
boolean resolveRefs)
{
int block = loadResourceValue(ident, outValue, resolveRefs);
if (block >= 0) {
if (outValue.type != TypedValue.TYPE_STRING) {
return true;
}
outValue.string = mStringBlocks[block].get(outValue.data);
return true;
}
return false;
}
......
}
這個函式定義在檔案frameworks/base/core/java/android/content/res/AssetManager.java中。
AssetManager類的成員函式getResourceValue通過呼叫另外一個成員函式loadResourceValue來載入引數ident所描述的資源。如果載入成功,那麼結果就會儲存在引數outValue所描述的一個TypedValue物件中,並且AssetManager類的成員函式loadResourceValue的返回值block大於等於0。
從前面Android應用程式資源管理器(Asset Manager)的建立過程分析一文可以知道,AssetManager類的成員變數mStringBlock描述的是一個StringBlock陣列。這個StringBlock陣列中的每一個StringBlock物件描述的都是當前應用程式使用的每一個資源索引表的資源項值字串資源池。關於資源索引表的格式以及生成過程,可以參考前面Android應用程式資源的編譯和打包過程分析一文。
瞭解了上述背景之後,我們就可以知道,當AssetManager類的成員函式loadResourceValue的返回值block大於等於0的時候,實際上就表示引數ident所描述的資源項在當前應用程式使用的第block個資源索引表中,而當引數ident所描述的資源項是一個字串時,那麼就可以在第block個資源索引表的資源項值字串資源池中找到對應的字串,並且儲存在引數outValue所描述的一個TypedValue物件的成員變數string中,以便返回給呼叫者使用。注意,最終得到的字串在第block個資源索引表的資源項值字串資源池中的位置就儲存在引數outValue所描述的一個TypedValue物件的成員變數data中。
接下來,我們就繼續分析AssetManager類的成員函式loadResourceValue的實現。
Step 8. AssetManager.loadResourceValue
[java] view plain copy print ?- public final class AssetManager {
- ......
- /** Returns true if the resource was found, filling in mRetStringBlock and
- * mRetData. */
- private native final int loadResourceValue(int ident, TypedValue outValue,
- boolean resolve);
- ......
- }
public final class AssetManager {
......
/** Returns true if the resource was found, filling in mRetStringBlock and
* mRetData. */
private native final int loadResourceValue(int ident, TypedValue outValue,
boolean resolve);
......
}
這個函式定義在檔案frameworks/base/core/java/android/content/res/AssetManager.java中。
AssetManager類的成員函式loadResourceValue是一個JNI方法,它是由C++層的函式android_content_AssetManager_loadResourceValue來實現的,如下所示:
[cpp] view plain copy print ?- static jint android_content_AssetManager_loadResourceValue(JNIEnv* env, jobject clazz,
- jint ident,
- jobject outValue,
- jboolean resolve)
- {
- AssetManager* am = assetManagerForJavaObject(env, clazz);
- if (am == NULL) {
- return 0;
- }
- const ResTable& res(am->getResources());
- Res_value value;
- ResTable_config config;
- uint32_t typeSpecFlags;
- ssize_t block = res.getResource(ident, &value, false, &typeSpecFlags, &config);
- ......
- uint32_t ref = ident;
- if (resolve) {
- block = res.resolveReference(&value, block, &ref);
- ......
- }
- return block >= 0 ? copyValue(env, outValue, &res, value, ref, block, typeSpecFlags, &config) : block;
- }
static jint android_content_AssetManager_loadResourceValue(JNIEnv* env, jobject clazz,
jint ident,
jobject outValue,
jboolean resolve)
{
AssetManager* am = assetManagerForJavaObject(env, clazz);
if (am == NULL) {
return 0;
}
const ResTable& res(am->getResources());
Res_value value;
ResTable_config config;
uint32_t typeSpecFlags;
ssize_t block = res.getResource(ident, &value, false, &typeSpecFlags, &config);
......
uint32_t ref = ident;
if (resolve) {
block = res.resolveReference(&value, block, &ref);
......
}
return block >= 0 ? copyValue(env, outValue, &res, value, ref, block, typeSpecFlags, &config) : block;
}
這個函式定義在檔案frameworks/base/core/jni/android_util_AssetManager.cpp中。
函式android_content_AssetManager_loadResourceValue主要是執行以下五個操作:
1. 呼叫函式assetManagerForJavaObject來將引數clazz所描述的一個Java層的AssetManager物件的成員變數mObject轉換為一個C++層的AssetManager物件。
2. 呼叫上述得到的C++層的AssetManager物件的成員函式getResources來獲得一個ResTable物件,這個ResTable物件描述的是一個資源表。
3. 呼叫上述得到的ResTable物件的成員函式getResource來獲得與引數ident所對應的資源項值及其配置資訊,並且儲存在型別為Res_value的變數value以及型別為ResTable_config的變數config中。
4. 如果引數resolve的值等於true,那麼就繼續呼叫上述得到的ResTable物件的成員函式resolveReference來解析前面所得到的資源項值。
5. 呼叫函式copyValue將上述得到的資源項值及其配置資訊拷貝到引數outValue所描述的一個Java層的TypedValue物件中去,返回呼叫者可以獲得與引數ident所對應的資源項內容。
接下來,我們就主要分析第2~4操作,即AssetManager物件的成員函式getResources以及ResTable類的成員函式getResource和resolveReference的實現,以便可以瞭解Android應用程式資源的查詢過程。
Step 9. AssetManager.getResources
[cpp] view plain copy print ?- const ResTable& AssetManager::getResources(bool required) const
- {
- const ResTable* rt = getResTable(required);
- return *rt;
- }
const ResTable& AssetManager::getResources(bool required) const
{
const ResTable* rt = getResTable(required);
return *rt;
}
這個函式定義在檔案frameworks/base/libs/utils/AssetManager.cpp中。
AssetManager類的成員函式getResources通過呼叫另外一個成員函式getResTable來獲得當前應用程式所使用的資源表,後者的實現如下所示:
[cpp] view plain copy print ?- const ResTable* AssetManager::getResTable(bool required) const
- {
- ResTable* rt = mResources;
- if (rt) {
- return rt;
- }
- // Iterate through all asset packages, collecting resources from each.
- AutoMutex _l(mLock);
- if (mResources != NULL) {
- return mResources;
- }
- ......
- const size_t N = mAssetPaths.size();
- for (size_t i=0; i<N; i++) {
- Asset* ass = NULL;
- ResTable* sharedRes = NULL;
- bool shared = true;
- const asset_path& ap = mAssetPaths.itemAt(i);
- Asset* idmap = openIdmapLocked(ap);
- ......
- if (ap.type != kFileTypeDirectory) {
- if (i == 0) {
- // The first item is typically the framework resources,
- // which we want to avoid parsing every time.
- sharedRes = const_cast<AssetManager*>(this)->
- mZipSet.getZipResourceTable(ap.path);
- }
- if (sharedRes == NULL) {
- ass = const_cast<AssetManager*>(this)->
- mZipSet.getZipResourceTableAsset(ap.path);
- if (ass == NULL) {
- ......
- ass = const_cast<AssetManager*>(this)->
- openNonAssetInPathLocked("resources.arsc",
- Asset::ACCESS_BUFFER,
- ap);
- if (ass != NULL && ass != kExcludedAsset) {
- ass = const_cast<AssetManager*>(this)->
- mZipSet.setZipResourceTableAsset(ap.path, ass);
- }
- }
- if (i == 0 && ass != NULL) {
- // If this is the first resource table in the asset
- // manager, then we are going to cache it so that we
- // can quickly copy it out for others.
- LOGV("Creating shared resources for %s", ap.path.string());
- sharedRes = new ResTable();
- sharedRes->add(ass, (void*)(i+1), false, idmap);
- sharedRes = const_cast<AssetManager*>(this)->
- mZipSet.setZipResourceTable(ap.path, sharedRes);
- }
- }
- } else {
- ......
- Asset* ass = const_cast<AssetManager*>(this)->
- openNonAssetInPathLocked("resources.arsc",
- Asset::ACCESS_BUFFER,
- ap);
- shared = false;
- }
- if ((ass != NULL || sharedRes != NULL) && ass != kExcludedAsset) {
- if (rt == NULL) {
- mResources = rt = new ResTable();
- updateResourceParamsLocked();
- }&nb