我們知道,在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應用程序資源的編譯和打包過程分析一文可以知道,APK包裏面的resources.arsc文件是在編譯應用程序資源的時候生成的,然後連同其它被編譯的以及原生的資源一起打包在一個APK包裏面。
從前面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
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
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
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
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
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
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
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
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來實現的,如下所示:
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
const ResTable& AssetManager::getResources(bool required) const { const ResTable* rt = getResTable(required); return *rt; }
這個函數定義在文件frameworks/base/libs/utils/AssetManager.cpp中。
AssetManager類的成員函數getResources通過調用另外一個成員函數getResTable來獲得當前應用程序所使用的資源表,後者的實現如下所示:
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(); } ...... if (sharedRes != NULL) { ...... rt->add(sharedRes); } else { ...... rt->add(ass, (void*)(i+1), !shared, idmap); } if (!shared) { delete ass; } } if (idmap != NULL) { delete idmap; } } ...... if (!rt) { mResources = rt = new ResTable(); } return rt; }
這個函數定義在文件frameworks/base/libs/utils/AssetManager.cpp中。
AssetManager類的成員函數getResources的實現看起來比較複雜,但是它要做的事情就是解析當前應用程序所使用的資源包裏面的resources.arsc文件。從前面Android應用程序資源管理器(Asset Manager)的創建過程分析一文可以知道,當前應用程序所使用的資源包有兩個,其中一個是系統資源包,即/system/framework/framework-res.apk,另外一個就是自己的APK文件。這些APK文件的路徑都分別使用一個asset_path對象來描述,並且保存在AssetManager類的成員變量mAssetPaths中。
AssetManager類的成員變量mResources指向的是一個ResTable對象,如果它的值不等於NULL,那麼就說明當前應用程序已經解析過它使用的資源包裏面的resources.arsc文件,因此,這時候AssetManager類的成員函數getResources就可以直接將該ResTable對象返回給調用者。
如果當前應用程序還沒有解析過它使用的資源包裏面的resources.arsc文件,那麼AssetManager類的成員函數getResources就會先獲取由成員變量mLock所描述的一個互斥鎖,避免多個線程同時去解析當前應用程序還沒有解析過它使用的資源包裏面的resources.arsc文件。注意,獲取鎖成功之後,有可能其它線程已經搶先一步解析了當前應用程序使用的資源包裏面的resources.arsc文件了,因此,這時候就需要再次判斷 AssetManager類的成員函數mResources是否等於NULL。如果不等於NULL,就可以將它所指向的ResTable對象返回給調用者了。
AssetManager類的成員函數getResources接下來按照以下步驟來解析當前應用程序所使用的每一個資源包裏面的resources.arsc文件:
1. 檢查資源包裏面的resources.arsc文件已經提取出來。如果已經提取出來的話,那麼以當前正在處理的資源包路徑爲參數,調用當前正在處理的AssetManager對象的成員變量mZipSet所指向的一個ZipSet對象的成員函數getZipResourceTableAsset就可以獲得一個對應的Asset對象。
2. 如果資源包裏面的resources.arsc文件還沒有提取出來,那麼就會調用當前正在處理的AssetManager對象的成員函數openNonAssetInPathLocked來將該resources.arsc文件提取出來。提取的結果就是獲得一個對應的Asset對象,保存在變量ass中。注意,如果當前提取出來的Asset對象的地址值不等於全局變量kExcludedAsset的值,那麼就將該Asset對象設置爲當前正在處理的AssetManager對象的成員變量mZipSet所指向的一個ZipSet對象中去,這是通過調用該ZipSet對象的成員函數setZipResourceTableAsset來實現的。
3. 將上面獲得的用來描述resources.arsc文件的Asset對象ass添加到變量rt所描述的一個ResTable對象中去,這是通過調用該ResTable對象的成員函數add來實現的。注意,如果該ResTable對象還沒有創建,那麼它就會首先被創建,並且同時保存在AssetManager類的成員變量mResources和變量rt中。另外一個地方需要注意的是,ResTable類的成員函數add在增加一個Asset對象時,會對該Asset對象所描述的resources.arsc文件的內容進行解析,結果就是得到一個系列的Package信息。每一個Package又包含了一個資源類型字符串資源池和一個資源項名稱字符串資源池,以及一系列的資源類型規範數據塊和一系列的資源項數據塊。這些內容可以參考前面Android應用程序資源的編譯和打包過程分析一文。還有第三個地方需要注意的是,每一個資源包裏面的所有Pacakge形成一個PackageGroup,保存在變量rt所指向的一個ResTable對象的成員變量mPackageGroups中。總之,ResTable類的作用就類似於在前面Android應用程序資源的編譯和打包過程分析一文所介紹的ResourceTable類。
一般來說,在AssetManager類的成員變量mAssetPaths中,第一個資源包路徑指向的就是系統資源包,而系統資源包在當前正在處理的AssetManager對象創建的時候,可能就已經提取或者初始化過了,也就是它的resources.arsc文件已經提取或者解析過了。如果已經解析過,那麼在代碼中的for循環中,當i等於0時,調用當前正在處理的AssetManager對象的成員變量mZipSet所指向的一個ZipSet對象的成員函數getZipResourceTable就可以獲得一個ResTable對象,並且保存在變量sharedRes中,最終就可以直接將該ResTable對象添加到變量rt所指向的一個ResTable對象中去,也就是添加到AssetManager類的成員變量mResources所指向的一個ResTable對象中去。如果只是提取過,但是還沒有解析過,那麼就會首先對它進行解析,並且將得到的ResTable對象保存在變量sharedRes中,最後再將該ResTable對象添加到變量rt所指向的一個ResTable對象中去。
此外,AssetManager類的成員變量mAssetPaths保存的資源包路徑指向可能不是一個APK文件,而是一個目錄文件。在這種情況下,AssetManager類的成員函數getResources就會直接調用另外一個成員函數openNonAssetInPathLocked來打開該目錄下的resources.arsc文件,並且獲得一個Asset對象,同樣是保存在變量ass中。
在前面Android應用程序資源管理器(Asset Manager)的創建過程分析一文還提到,Android系統的資源管理框架提供了一種idmap機制,用來個性化定製一個資源包裏面已有的資源項,也就是說,每一個資源包都可能有一個對應的idmap文件,用來描述它所個性化定製的資源項。在提取和解析資源包的過程中,如果該資源包存在idmap文件,那麼該idmap文件也會被解析,並且解析得到的一個Asset對象也會同時被增加到變量rt所指向的一個ResTable對象中去。
經過上述的一系列操作之後,AssetManager類的成員變量mResources所指向的一個ResTable對象就包含了當前應用程序所使用的資源包的所有信息,該ResTable對象最後就會返回給調用者來使用。
這一步執行完成之後,回到前面的Step 8中,即AssetManager類的成員函數loadResourceValue中,接下來它就會調用前面所獲得的一個ResTable對象的成員函數getResource來獲得指定的資源項內容。
Step 10. ResTable.getResource
ssize_t ResTable::getResource(uint32_t resID, Res_value* outValue, bool mayBeBag, uint32_t* outSpecFlags, ResTable_config* outConfig) const { ...... const ssize_t p = getResourcePackageIndex(resID); const int t = Res_GETTYPE(resID); const int e = Res_GETENTRY(resID); ...... const Res_value* bestValue = NULL; const Package* bestPackage = NULL; ResTable_config bestItem; memset(&bestItem, 0, sizeof(bestItem)); // make the compiler shut up if (outSpecFlags != NULL) *outSpecFlags = 0; // Look through all resource packages, starting with the most // recently added. const PackageGroup* const grp = mPackageGroups[p]; ...... size_t ip = grp->packages.size(); while (ip > 0) { ip--; int T = t; int E = e; const Package* const package = grp->packages[ip]; if (package->header->resourceIDMap) { uint32_t overlayResID = 0x0; status_t retval = idmapLookup(package->header->resourceIDMap, package->header->resourceIDMapSize, resID, &overlayResID); if (retval == NO_ERROR && overlayResID != 0x0) { // for this loop iteration, this is the type and entry we really want ...... T = Res_GETTYPE(overlayResID); E = Res_GETENTRY(overlayResID); } else { // resource not present in overlay package, continue with the next package continue; } } const ResTable_type* type; const ResTable_entry* entry; const Type* typeClass; ssize_t offset = getEntry(package, T, E, &mParams, &type, &entry, &typeClass); if (offset <= 0) { // No {entry, appropriate config} pair found in package. If this // package is an overlay package (ip != 0), this simply means the // overlay package did not specify a default. // Non-overlay packages are still required to provide a default. if (offset < 0 && ip == 0) { ...... return offset; } continue; } if ((dtohs(entry->flags)&entry->FLAG_COMPLEX) != 0) { ...... continue; } ...... const Res_value* item = (const Res_value*)(((const uint8_t*)type) + offset); ResTable_config thisConfig; thisConfig.copyFromDtoH(type->config); if (outSpecFlags != NULL) { if (typeClass->typeSpecFlags != NULL) { *outSpecFlags |= dtohl(typeClass->typeSpecFlags[E]); } else { *outSpecFlags = -1; } } if (bestPackage != NULL && (bestItem.isMoreSpecificThan(thisConfig) || bestItem.diff(thisConfig) == 0)) { // Discard thisConfig not only if bestItem is more specific, but also if the two configs // are identical (diff == 0), or overlay packages will not take effect. continue; } bestItem = thisConfig; bestValue = item; bestPackage = package; } ...... if (bestValue) { outValue->size = dtohs(bestValue->size); outValue->res0 = bestValue->res0; outValue->dataType = bestValue->dataType; outValue->data = dtohl(bestValue->data); if (outConfig != NULL) { *outConfig = bestItem; } ...... return bestPackage->header->index; } return BAD_VALUE; }
這個函數定義在文件frameworks/base/libs/utils/ResourceTypes.cpp中。
參數resID描述的是要查找的資源的ID,ResTable類的成員函數getResource分別獲得它的Pakcage ID、Type ID以及Entry ID,保存在變量p、t以及e中。知道了Pakcage ID之後,就可以在ResTable類的成員變量mPackageGroups中找到對應的PakcageGroup。
注意,前面獲得的PakcageGroup可能包含有多個Package,這些Package都保存在PakcageGroup的成員變量packages所描述的一個數組中,因此,ResTable類的成員函數getResource就通過一個while循環來在每一個Package中查找最符合條件的資源項。
如果當前正在處理的Package的成員變量header所描述的一個Header對象的成員變量resourceIDMap的值不等於NULL,那麼它所指向的就是一個idmap,同時也說明當前正在處理的Package是一個Overlay Package,也就是說,它是用來覆蓋已存在的資源項的。在這種情況下,ResTable類的成員函數getResource就會將參數resID所描述的資源ID映射爲覆蓋後的資源ID。注意,如果不能將參數resID所描述的資源ID映射爲覆蓋後的ID,那麼當前正在處理的Package就會被跳過。
無論當前正在處理的Package是否是一個Overlay Package,最要要找到的資源項的Type ID和Entry ID都保存在變量T和E中,接下來ResTable類的成員函數getResource就會以這兩個變量爲參數來調用另外一個成員函數getEntry來在當前正在處理的Package中檢查是否存在符合條件的資源項。如果存在的話,那麼調用ResTable類的成員函數getEntry得到的返回值offset就會大於0,同時還會得到三個類型分別爲ResTable_type、ResTable_entry和Type結構體,分別保存在變量type、entry和typeClass中。其中,ResTable_type用來描述一個資源類型,ResTable_entry用來描述一個資源項,而Type用來描述一個資源類型規範,關於這些結構的詳細解釋可以參考前面Android應用程序資源的編譯和打包過程分析一文。
從前面Android應用程序資源的編譯和打包過程分析一文還可以知道,對於一個普通的資源項來說,它在資源表文件resources.arsc中,由一個ResTable_entry和一個Res_value結構體前面連接在一起表示,其中,結構體ResTable_entry用來描述資源項的頭部,而結構體Res_value用來描述資源項的值,並且這兩個結構體是嵌入在一個ResTable_type結構體裏面的。
理解了上述背景知道之後,我們就可以解釋前面得到的返回值offset的含義了,它表示一個Res_value結構體在一個ResTable_type結構體中的偏移,也就是說,將前面獲得的ResTable_type結構體type的開始地址,再加偏移量offset,就可以得到一個Res_value結構體item,而這個Res_value結構體就是表示資源ID等於resID的資源項的值。
注意,在調用ResTable類的成員函數getEntry來在當前正在處理的Package中查找與參數resID對應的資源項時,還會指定設備的當前配置信息。設備的當前配置信息是由ResTable類的成員變量mParams所指向的一個ResTable_config結構體來描述的。如果ResTable類的成員函數getEntry能成功找到一個匹配的資源項,那麼它還會通過ResTable_type結構體type的成員變量config所指向的一個ResTable_config結構體來返回該資源項的實際配置信息,並且保存在另外一個ResTable_config結構體thisConfig中。
現在一切就準備就緒了,ResTable類的成員函數getResource要做的事情就是比較在前後兩個Package中找到的兩個資源項中,哪一個資源項更匹配設備的當前配置信息。注意,在前一個Package中找到的資源項的值及其所對應的Package和配置信息分別保存在Res_value結構體bestValue、Package結構體bestPackage和ResTable_config結構體bestItem中,而在後一個Package中找到的資源項對應的配置信息保存在ResTable_config結構體thisConfig中。
如果ResTable_config結構體bestItem描述的配置信息比ResTable_config結構體thisConfig描述的配置信息更具體,或者它們完全是一樣的,那麼ResTable類的成員函數getResource就會認爲在前一個Package中找到的資源項更匹配設備的當前配置信息,於是就保持Res_value結構體bestValue、Package結構體bestPackage和ResTable_config結構體bestItem的值不變,否則的話,就會將在後一個Package中找到的資源項的值及其所對應的Package和配置信息分別保存在Res_value結構體bestValue、Package結構體bestPackage和ResTable_config結構體bestItem中。
ResTable類的成員函數getResource執行完成中間的while循環之後,最終得到的資源項的值以及配置信息就保存在Res_value結構體bestValue和ResTable_config結構體bestItem,最後就可以分別將它們的內容拷貝到輸出參數outValue和outConfig中去,以便可以返回給調用者。同時,ResTable類的成員函數getResource還會將最終得到的資源項所在的Package的索引返回給調用者。通過這個Package的索引值,調用者就可以知道它所找到的資源項是在哪一個資源包中找到的,例如,是在系統資源包找到的,還是在應用程序本身的資源包找到的。
事實上,ResTable類的成員函數getResource返回給調用者的還有一個很重要的信息,那就是參數resID所描述的資源項的配置狀況,也就是說,參數resID所描述的資源項的配置差異性信息。這個配置差異性信息就保存在前面得到的Type結構體typeClass的成員變量typeSpecFlags所描述的一個uint32_t數組中的第E個元素中,這是因爲參數resID所描述的資源項的Entry ID等於E。關於資源項的配置差異性信息的詳細描述,可以參考前面Android應用程序資源的編譯和打包過程分析一文所提到的ResTable_typeSpec結構體。
在ResTable類的成員函數getResource查找資源的過程中,還有兩個地方是需要注意的。
第一個地方是ResTable類的成員函數getResource是從後往前遍歷Pakcage ID等於p的PackageGroup中的每一個Package的。在一個PackageGroup中,第一個Package是一個Base Package,其它的Package都是屬於Overlay Package,其中,在Overlay Package中定義的資源是用來覆蓋在Base Package中定義的資源的。如果ResTable類的成員函數getResource在調用另外一個成員函數getEntry來在某一個package中找不到對應的資源項時,即調用ResTable類的成員函數getEntry得到的返回值offset小於等於0的時候,需要進一步檢查該package是一個Base Package還是一個Overlay Package。如果是一個Overlay Package,那麼這種情況是允許發生的,因爲一個Overlay Package只需要定義它需要覆蓋的資源。另一方面,如果是一個Base Package,那麼就是這種情況就是異常的,因爲一個Base Package無論如何都要保證在給定一個合法的資源ID的前提下,一定可以找到一個對應的資源項,在最壞的情況下,這個資源項就是一個default類型的。
第二個地方是隻有當調用ResTable類的成員函數getEntry得到資源項是一個普通資源項時,即得到的ResTable_entry結構體entry的成員變量flags的值的FLAG_COMPLEX位等於0時,纔可以將得到的ResTable_type結構體type的偏移位置offset轉換爲一個Res_value結構體來訪問。這是因爲如果找到的資源項不是一個普通資源項,而是一個Bag資源項時,得到的ResTable_type結構體type的偏移位置offset是一個ResTable_map結構體數組,而不是一個Res_value結構體,這一點可以參考前面Android應用程序資源的編譯和打包過程分析一文。
接下來,我們就繼續分析ResTable類的成員函數getEntry的實現,以便可以瞭解它是如何在一個Package中找到指定Type ID、Entry ID以及配置信息的資源項的,如下所示:
ssize_t ResTable::getEntry( const Package* package, int typeIndex, int entryIndex, const ResTable_config* config, const ResTable_type** outType, const ResTable_entry** outEntry, const Type** outTypeClass) const { ...... const ResTable_package* const pkg = package->package; const Type* allTypes = package->getType(typeIndex); ...... const ResTable_type* type = NULL; uint32_t offset = ResTable_type::NO_ENTRY; ResTable_config bestConfig; memset(&bestConfig, 0, sizeof(bestConfig)); // make the compiler shut up const size_t NT = allTypes->configs.size(); for (size_t i=0; i<NT; i++) { const ResTable_type* const thisType = allTypes->configs[i]; if (thisType == NULL) continue; ResTable_config thisConfig; thisConfig.copyFromDtoH(thisType->config); ...... // Check to make sure this one is valid for the current parameters. if (config && !thisConfig.match(*config)) { ...... continue; } // Check if there is the desired entry in this type. const uint8_t* const end = ((const uint8_t*)thisType) + dtohl(thisType->header.size); const uint32_t* const eindex = (const uint32_t*) (((const uint8_t*)thisType) + dtohs(thisType->header.headerSize)); uint32_t thisOffset = dtohl(eindex[entryIndex]); if (thisOffset == ResTable_type::NO_ENTRY) { ...... continue; } if (type != NULL) { // Check if this one is less specific than the last found. If so, // we will skip it. We check starting with things we most care // about to those we least care about. if (!thisConfig.isBetterThan(bestConfig, config)) { ...... continue; } } type = thisType; offset = thisOffset; bestConfig = thisConfig; ...... if (!config) break; } if (type == NULL) { ...... return BAD_INDEX; } offset += dtohl(type->entriesStart); ...... const ResTable_entry* const entry = (const ResTable_entry*) (((const uint8_t*)type) + offset); ...... *outType = type; *outEntry = entry; if (outTypeClass != NULL) { *outTypeClass = allTypes; } return offset + dtohs(entry->size); }
這個函數定義在文件frameworks/base/libs/utils/ResourceTypes.cpp中。
ResTable類的成員函數getEntry首先是在參數pakcage所描述的一個Package中找到與參數typeIndex所對應的一個Type結構體allTypes。這個Type結構體綜合了我們在前面Android應用程序資源的編譯和打包過程分析一文所描述的資源項的ResTable_typeSpec數據塊和ResTable_type數據塊,也就是,這個Type結構體的成員變量typeSpecFlags所指向的一個uint32_t數組描述了同一種類型的所有資源項的配置差異性性信息,即相當於一系列ResTable_typeSpec數據塊,而成員變量configs所指向的一個ResTable_type數組描述的同一種類型的資源項的具體內容,即相當於一系列ResTable_type數據塊。
結合前面Android應用程序資源的編譯和打包過程分析一文對ResTable_typeSpec數據塊和ResTable_type數據塊的描述,我們就可以比較容易理解ResTable類的成員函數getEntry的實現了。由於Type結構體allTypes的成員變量configs所指向的一個ResTable_type數組描述的所有類型爲typeIndex的資源項的具體內容,因此,ResTable類的成員函數getEntry只要遍歷這個ResTable_type數組,並且找到與參數entryIdex所對應的最匹配資源項即可,也就是最能匹配參數config所描述的配置信息的資源項即可。
我們知道,在每一個ResTable_type結構體中,都包含有一個類型爲uint32_t的偏移數組。偏移數組中的第entryIndex個元素的值就表示Entry ID等於entryIndex的資源項的具體內容相對於ResTable_type結構體的偏移值。有了這個偏移值之後,我們就得到Entry ID等於entryIndex的資源項的具體內容了,也就是得到一個對應的ResTable_entry結構體。
注意,每一個ResTable_type結構體都有一個成員變量config,它描述的是一個ResTable_config對象,表示該ResTable_type結構體的配置信息。如果該配置信息要求的配置信息不匹配,也就是與參數config所描述的設置配置信息不匹配,那麼就不需要進一步檢查該ResTable_type結構體是否存在Entry ID等於entryIndex的資源項了。
如果一個ResTable_type結構體的配置信息與參數config所描述的設置配置信息匹配,但是它不存在Entry ID等於entryIndex的資源項,即它的偏移數組中的第entryIndex個元素的值等於ResTable_type::NO_ENTRY,那麼也不需要進一步對該ResTable_type結構體進行處理了。
如果一個ResTable_type結構體的配置信息與參數config所描述的設置配置信息匹配,並且它也存在Entry ID等於entryIndex的資源項,那麼就需要比較前後兩個ResTable_type結構體的配置信息與參數config所描述的設置配置信息相比,哪一個更具體一些。具有更具體的配置信息的資源項將作爲最匹配的資源項返回給調用者。注意,前一個ResTable_type結構體及其配置信息分別保存在變量type和bestConfig中,並且在該ResTable_type結構體中Entry ID等於entryIndex的資源項相對它的偏移值保存在變量offset中。
遍歷完成上面所述的ResTable_type數組之後,最終得到的最區配資源項的相關信息就保存在變量type、bestConfig和offset中。這時候將變量type所描述的ResTable_type結構體的起始位置,加上它的成員變量entriesStart的值,以及再加上變量offset的值,就可以得到一個對應的ResTable_entry結構體entry。這個ResTable_entry結構體entry就是用來描述最終在參數package所描述的Package中,找到與參數typeIndex、entryIndex和config最匹配的資源項的信息了。
最終,ResTable類的成員函數getEntry就將前面所得到的ResTable_entry結構體entry、ResTable_type結構體type以及Type結構體allTypes分別保存在輸出參數outEntry、outType和outTypeClass返回給調用者了。同時,ResTable類的成員函數getEntry還會將變量offset的值加上ResTable_entry結構體entry的大小的結果返回給調用者。調用都得到這個結果之後,就可以將它作爲輸出參數outType所指向的ResTable_type結構體的偏移量,從而可以得到參數outEntry所指向的ResTable_entry結構體所描述的資源項的具體內容,也就是得到一個Res_value結構體,這個具體就可以參考前面對ResTable類的成員函數getResource的分析。
這一步執行完成之後,回到前面的Step 8中,即AssetManager類的成員函數loadResourceValue中,接下來它就會調用前面所獲得的一個ResTable對象的成員函數resolveReference來解析前面所獲得的資源項的內容了。
Step 11. ResTable.resolveReference
ssize_t ResTable::resolveReference(Res_value* value, ssize_t blockIndex, uint32_t* outLastRef, uint32_t* inoutTypeSpecFlags, ResTable_config* outConfig) const { int count=0; while (blockIndex >= 0 && value->dataType == value->TYPE_REFERENCE && value->data != 0 && count < 20) { if (outLastRef) *outLastRef = value->data; uint32_t lastRef = value->data; uint32_t newFlags = 0; const ssize_t newIndex = getResource(value->data, value, true, &newFlags, outConfig); if (newIndex == BAD_INDEX) { return BAD_INDEX; } ...... //printf("Getting reference 0x%08x: newIndex=%d\n", value->data, newIndex); if (inoutTypeSpecFlags != NULL) *inoutTypeSpecFlags |= newFlags; if (newIndex < 0) { // This can fail if the resource being referenced is a style... // in this case, just return the reference, and expect the // caller to deal with. return blockIndex; } blockIndex = newIndex; count++; } return blockIndex; }
這個函數定義在文件frameworks/base/libs/utils/ResourceTypes.cpp中。
ResTable類的成員函數resolveReference的實現其實很簡單,它就是對參數value所描述的一個資源項值進行解析,前提是這個資源項值是一個引用,即value所指向的一個Res_value結構體的成員變量dataType的值等於TYPE_REFERENCE,因爲如果不是引用類型的資源項值,就沒有必要解析了。
注意,一個資源項的值有可能是嵌套引用的,也就是可能是引用的引用,因此,ResTable類的成員函數resolveReference需要使用一個while循環來不斷地對參數value所描述的一個資源項值進行解析,直到最後一次解析出來的結果不是引用爲止。但是,爲了防止無限地解析下去,該while循環最多隻允許執行20次。每一次解析都是調用ResTable類的成員函數getResource來實現的,這個成員函數我們在前面的Step 10中已經分析過了。
此外,上述while循環還有兩個條件需要滿足。第一個條件是每一次調用ResTable類的成員函數getResource來解析參數value所描述的資源項值之後,得到的返回值blockIndex都必須是大於等於0的,這是因爲它表示該次解析是成功的。由於參數blockIndex最開始的值是由調用者傳進來的,因此,也要保證它最開始的值大於等於0,纔會執行代碼中的while循環對參數value所描述的資源項值進行解析。第二個條件是參數value所描述的資源項的值不能等於0,即它所指向的一個Res_value結構體的成員變量data的值不能等於0,這是因爲引用者都是不可能等於0的。
這一步執行完成之後,沿着調用路徑最終返回到前面的Step 5中,即Resources類的成員函數loadXmlResourceParser中,我們就可以得到參數id所描述的資源項的值了。在我們這個情景中,參數id描述的是一個layout資源ID,它所對應的資源項的值是一個字符串,這個字符串描述的便是一個UI佈局文件,即一個經過編譯的、以二進制格式保存的Xml資源文件。有了這個Xml資源文件的路徑之後,Resources類的另外一個四個參數版本的成員函數loadXmlResourceParser就會被調用來對該Xml資源文件進行解析,以便可以得到一個UI佈局視圖。
Step 12. Resources.loadXmlResourceParser
public class Resources { ...... private int mLastCachedXmlBlockIndex = -1; private final int[] mCachedXmlBlockIds = { 0, 0, 0, 0 }; private final XmlBlock[] mCachedXmlBlocks = new XmlBlock[4]; ...... /*package*/ XmlResourceParser loadXmlResourceParser(String file, int id, int assetCookie, String type) throws NotFoundException { if (id != 0) { try { // These may be compiled... synchronized (mCachedXmlBlockIds) { // First see if this block is in our cache. final int num = mCachedXmlBlockIds.length; for (int i=0; i<num; i++) { if (mCachedXmlBlockIds[i] == id) { ...... return mCachedXmlBlocks[i].newParser(); } } // Not in the cache, create a new block and put it at // the next slot in the cache. XmlBlock block = mAssets.openXmlBlockAsset( assetCookie, file); if (block != null) { int pos = mLastCachedXmlBlockIndex+1; if (pos >= num) pos = 0; mLastCachedXmlBlockIndex = pos; XmlBlock oldBlock = mCachedXmlBlocks[pos]; if (oldBlock != null) { oldBlock.close(); } mCachedXmlBlockIds[pos] = id; mCachedXmlBlocks[pos] = block; ...... return block.newParser(); } } } catch (Exception e) { NotFoundException rnf = new NotFoundException( "File " + file + " from xml type " + type + " resource ID #0x" + Integer.toHexString(id)); rnf.initCause(e); throw rnf; } } throw new NotFoundException( "File " + file + " from xml type " + type + " resource ID #0x" + Integer.toHexString(id)); } ...... }
這個函數定義在文件frameworks/base/core/java/android/content/res/Resources.java中
Resources類有三個成員變量是與Xml資源文件緩存文件有關的,它們分別是mCachedXmlBlocks、mCachedXmlBlockIds和mLastCachedXmlBlockIndex。其中,mCachedXmlBlocks指向的是一個大小等於4的XmlBlock數組,mCachedXmlBlockIds指向的也是一個大小等於4的資源ID數組,而mLastCachedXmlBlockIndex表示上一次緩存的Xml資源文件在上述XmlBlock數組和資源ID數組中的索引。這就是說,Resources類最多可以緩存最近讀取的四個Xml資源文件的內容,讀取超過四個Xml資源文件之後 ,上述的XmlBlock數組和資源ID數組就會被循環利用。
理解了Resources類的上述三個成員變量的含義之後,Resources類的成員函數loadXmlResourceParser的實現就容易理解了。它首先是在成員變量mCachedXmlBlockIds所指向的資源ID數組中檢查是否存在一個資源ID與參數id所描述的資源ID相等。如果存在的話,那麼就會在成員變量mCachedXmlBlocks所指向的XmlBlock數組中找到一個對應的XmlBlock對象,並且調用這個XmlBlock對象的成員函數newParser來創建一個XmlResourceParser對象返回給調用者。
如果在Resources類的成員變量mCachedXmlBlockIds所指向的資源ID數組找到對應的資源ID的話,那麼Resources類的成員函數loadXmlResourceParser就會調用成員變量mAssets所指向的一個Java層的AssetManager對象的成員函數openXmlBlockAsset來打開參數file所指定的Xml資源文件,從而獲得一個XmlBlock對象block。這個XmlBlock對象block以及參數id所描述的資源ID同時也會被緩存在Resources類的成員變量mCachedXmlBlocks和mCachedXmlBlockIds所描述的XmlBlock數組和資源ID數組中。
最後,Resources類的成員函數loadXmlResourceParser就可以調用前面得到的XmlBlock對象block的成員函數newParser來創建一個XmlResourceParser對象返回給調用者。
接下來,我們就首先分析Java層的AssetManager類的成員函數openXmlBlockAsset的實現,接着再分析XmlBlock類的成員函數newParser的實現,以便可以瞭解一個Xml資源文件的打開過程。
Step 13. AssetManager.openXmlBlockAsset
public final class AssetManager { ...... /*package*/ final XmlBlock openXmlBlockAsset(int cookie, String fileName) throws IOException { synchronized (this) { if (!mOpen) { throw new RuntimeException("Assetmanager has been closed"); } int xmlBlock = openXmlAssetNative(cookie, fileName); if (xmlBlock != 0) { XmlBlock res = new XmlBlock(this, xmlBlock); incRefsLocked(res.hashCode()); return res; } } throw new FileNotFoundException("Asset XML file: " + fileName); } ...... }
這個函數定義在文件frameworks/base/core/java/android/content/res/AssetManager.java中。
AssetManager類的成員函數openXmlBlockAsset首先檢查成員變量mOpen的值是否不等於true。如果不等於true的話,那麼就說明當前正在處理的AssetManager對象還沒有經過初始化,或者已經關閉了。
我們假設AssetManager類的成員變量mOpen的值等於true,那麼接下來AssetManager類的成員函數openXmlBlockAsset就會調用另外一個成員函數openXmlAssetNative來打開參數fileName所指定的Xml資源文件。
成功打開參數fileName所指向的Xml資源文件之後,就會得到一個C++層的ResXMLTree對象的地址值xmlBlock,最後AssetManager類的成員函數openXmlBlockAsset就將該C++層的ResXMLTree對象的地址值封裝在一個Java層的XmlBlock對象中,並且將該XmlBlock對象返回給調用者。
接下來,我們就繼續分析AssetManager類的成員函數openXmlAssetNative的實現。
Step 14. AssetManager.openXmlAssetNative
public final class AssetManager { ...... private native final int openXmlAssetNative(int cookie, String fileName); ...... }
這個函數定義在文件frameworks/base/core/java/android/content/res/AssetManager.java中。
AssetManager類的成員函數openXmlAssetNative是一個JNI方法,它是C++層的函數android_content_AssetManager_openXmlAssetNative來實現的,如下所示:
static jint android_content_AssetManager_openXmlAssetNative(JNIEnv* env, jobject clazz, jint cookie, jstring fileName) { AssetManager* am = assetManagerForJavaObject(env, clazz); ...... const char* fileName8 = env->GetStringUTFChars(fileName, NULL); Asset* a = cookie ? am->openNonAsset((void*)cookie, fileName8, Asset::ACCESS_BUFFER) : am->openNonAsset(fileName8, Asset::ACCESS_BUFFER); ...... env->ReleaseStringUTFChars(fileName, fileName8); ResXMLTree* block = new ResXMLTree(); status_t err = block->setTo(a->getBuffer(true), a->getLength(), true); a->close(); delete a; ...... return (jint)block; }
這個函數定義在文件frameworks/base/core/jni/android_util_AssetManager.cpp中。
函數android_content_AssetManager_openXmlAssetNative首先調用另外一個函數assetManagerForJavaObject來將參數clazz所指向的一個Java層的AssetManager對象成員變量mObject轉換爲一個C++層的AssetManager對象。有了這個C++層的AssetManager對象之後,就可以調用它的成員函數openNonAsset來打開參數fileName所指定的Xml資源文件。
調用C++層的AssetManager對象的成員函數openNonAsset來成功地打開參數fileName所指定的Xml資源文件之後,函數android_content_AssetManager_openXmlAssetNative接下來就會創建一個ResXMLTree對象,並且將前面所打開的Xml資源文件的內容設置到該ResXMLTree對象中去,並且將該ResXMLTree對象的地址值返回給調用者。
假設參數cookie的值大於0,因此,函數android_content_AssetManager_openXmlAssetNative實際調用的是C++層的AssetManager類的三個參數版本的成員函數openNonAsset來打開參數fileName所指定的Xml資源文件,接下來,我們就分析這個函數的實現。
Step 15. AssetManager.openNonAsset
Asset* AssetManager::openNonAsset(void* cookie, const char* fileName, AccessMode mode) { const size_t which = ((size_t)cookie)-1; AutoMutex _l(mLock); ...... if (which < mAssetPaths.size()) { ...... Asset* pAsset = openNonAssetInPathLocked( fileName, mode, mAssetPaths.itemAt(which)); if (pAsset != NULL) { return pAsset != kExcludedAsset ? pAsset : NULL; } } return NULL; }
這個函數定義在文件frameworks/base/libs/utils/AssetManager.cpp中。
參數cookie是用來標識另外一個參數fileName所指向的文件是屬於哪個APK文件的。從前面Android應用程序資源管理器(Asset Manager)的創建過程分析一文可以知道,參數cookie實際上是一個整數,將這個整數減去1之後,得到的結果就是參數fileName所指向的文件所屬於的APK文件在AssetManager類的成員變量mAssetPaths所描述的一個數組的索引。AssetManager類的成員變量mAssetPaths描述的是一個asset_path數組,數組中的每一個元素都是表示一個APK文件路徑,這些APK文件就包含了當前應用程序所要使用的資源包。
AssetManager類的成員函數openNonAsset通過參數cookie知道了當前要打開的文件是位於哪個APK文件之後,接着就繼續調用另外一個成員函數openNonAssetInPathLocked來打開該文件。
Step 16. AssetManager.openNonAssetInPathLocked
Asset* AssetManager::openNonAssetInPathLocked(const char* fileName, AccessMode mode, const asset_path& ap) { Asset* pAsset = NULL; /* look at the filesystem on disk */ if (ap.type == kFileTypeDirectory) { String8 path(ap.path); path.appendPath(fileName); pAsset = openAssetFromFileLocked(path, mode); if (pAsset == NULL) { /* try again, this time with ".gz" */ path.append(".gz"); pAsset = openAssetFromFileLocked(path, mode); } if (pAsset != NULL) { //printf("FOUND NA '%s' on disk\n", fileName); pAsset->setAssetSource(path); } /* look inside the zip file */ } else { String8 path(fileName); /* check the appropriate Zip file */ ZipFileRO* pZip; ZipEntryRO entry; pZip = getZipFileLocked(ap); if (pZip != NULL) { //printf("GOT zip, checking NA '%s'\n", (const char*) path); entry = pZip->findEntryByName(path.string()); if (entry != NULL) { //printf("FOUND NA in Zip file for %s\n", appName ? appName : kAppCommon); pAsset = openAssetFromZipLocked(pZip, entry, mode, path); } } if (pAsset != NULL) { /* create a "source" name, for debug/display */ pAsset->setAssetSource( createZipSourceNameLocked(ZipSet::getPathName(ap.path.string()), String8(""), String8(fileName))); } } return pAsset; }
這個函數定義在文件frameworks/base/libs/utils/AssetManager.cpp中。
AssetManager類的成員函數openNonAssetInPathLocked的實現是比較簡單的,它按照以下兩種方式來打開參數fileName所指向的文件。
如果參數ap描述的文件路徑是一個目錄,那麼它就會直接將參數fileName所指向的文件名稱附加到該目錄後面去,然後直接調用另外一個成員函數openAssetFromFileLocked來打開參數fileName所指向的文件。如果打開失敗,那麼就再假定參數ap描述的文件路徑是一個以“.gz“爲後綴的壓縮包,然後再次調用成員函數openAssetFromFileLocked來打開參數fileName所指向的文件。
如果參數ap描述的文件路徑是一個普通文件,那麼就意味着參數ap描述的是一個壓縮文件,因此,它就會先調用成員函數getZipFileLocked來打開該壓縮文件,然後再調用成員函數openAssetFromZipLocked來在該壓縮包將參數fileName所指向的文件提取出來。
Android應用程序的資源一般都是打包在一個APK文件裏面的,而APK文件就是一個Zip格式的壓縮包,因此,AssetManager類的成員函數openNonAssetInPathLocked一般就是按照第二種方式來打開參數fileName所指向的文件。
接下來,我們就繼續分析AssetManager類的成員函數openAssetFromZipLocked的實現。
Step 17. AssetManager.openAssetFromZipLocked
Asset* AssetManager::openAssetFromZipLocked(const ZipFileRO* pZipFile, const ZipEntryRO entry, AccessMode mode, const String8& entryName) { Asset* pAsset = NULL; // TODO: look for previously-created shared memory slice? int method; size_t uncompressedLen; ...... if (!pZipFile->getEntryInfo(entry, &method, &uncompressedLen, NULL, NULL, NULL, NULL)) { LOGW("getEntryInfo failed\n"); return NULL; } FileMap* dataMap = pZipFile->createEntryFileMap(entry); if (dataMap == NULL) { LOGW("create map from entry failed\n"); return NULL; } if (method == ZipFileRO::kCompressStored) { pAsset = Asset::createFromUncompressedMap(dataMap, mode); LOGV("Opened uncompressed entry %s in zip %s mode %d: %p", entryName.string(), dataMap->getFileName(), mode, pAsset); } else { pAsset = Asset::createFromCompressedMap(dataMap, method, uncompressedLen, mode); LOGV("Opened compressed entry %s in zip %s mode %d: %p", entryName.string(), dataMap->getFileName(), mode, pAsset); } if (pAsset == NULL) { /* unexpected */ LOGW("create from segment failed\n"); } return pAsset; }
這個函數定義在文件frameworks/base/libs/utils/AssetManager.cpp中。
AssetManager類的成員函數openAssetFromZipLocked的實現也很簡單,它無非就是從參數pZipFile所描述的壓縮包中將參數entryName所描述的文件提取出來,並且根據提取出來的內容保存在一個Asset對象中返回給調用者。
注意,參數entryName所描述的文件有可能是經過是經過壓縮後再打包到參數pZipFile所描述的壓縮包去的。在這種情況下,AssetManager類的成員函數openAssetFromZipLocked就會調用Asset類的靜態成員函數createFromCompressedMap來對它進行解壓,然後再將解壓完成後得到的內容保存在一個Asset對象中。否則的話,AssetManager類的成員函數openAssetFromZipLocked就會調用Asset類的靜態成員函數createFromUncompressedMap來將它的內容保存在一個Asset對象中。
這一步執行完成之後,返回到前面的Step 12中,即Resources類的成員函數loadXmlResourceParser中,接下來就會調用Java層的XmlBlock類的成員函數newParser來創建一個XmlResourceParser對象,以便可以用來解析前面打開的Xml資源文件。
Step 18. XmlBlock.newParser
final class XmlBlock { ...... public XmlResourceParser newParser() { synchronized (this) { if (mNative != 0) { return new Parser(nativeCreateParseState(mNative), this); } return null; } } ...... private final int mNative; ...... private static final native int nativeCreateParseState(int obj); ...... }
這個函數定義在文件frameworks/base/core/java/android/content/res/XmlBlock.java中。
XmlBlock類的成員函數mNative指向的是C++層的一個ResXMLTree對象。這個ResXMLTree對象是在前面的Step 14中創建的,用來描述前面所打開的一個Xml資源文件。
XmlBlock類的成員函數newParser首先是以成員變量mNative所描述的一個C++層的ResXMLTree對象的地址爲參數,來調用JNI方法nativeCreateParseState,用來在C++層創建一個ResXMLParser對象,最後再將該C++層的ResXMLParser對象封裝成Java層的一個Parser對象中,並且將該Parser對象返回給調用者。
這一步執行完成之後,返回到前面的Step 3中,即LayoutInflater類的成員函數inflate中,接下來它就會以前面所獲得的一個Java層的Parser對象來參數,來調用另外一個重載版本的成員函數inflate,用來解析前面所打開的Xml資源文件,即一個UI佈局文件,以便可以創建相應的UI佈局出來。
Step 19. LayoutInflater.inflate
public abstract class LayoutInflater { ...... public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) { synchronized (mConstructorArgs) { final AttributeSet attrs = Xml.asAttributeSet(parser); Context lastContext = (Context)mConstructorArgs[0]; mConstructorArgs[0] = mContext; View result = root; try { // Look for the root node. int type; while ((type = parser.next()) != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT) { // Empty } if (type != XmlPullParser.START_TAG) { throw new InflateException(parser.getPositionDescription() + ": No start tag found!"); } final String name = parser.getName(); ...... if (TAG_MERGE.equals(name)) { ...... rInflate(parser, root, attrs); } else { // Temp is the root view that was found in the xml View temp = createViewFromTag(name, attrs); ViewGroup.LayoutParams params = null; if (root != null) { ...... // Create layout params that match root, if supplied params = root.generateLayoutParams(attrs); if (!attachToRoot) { // Set the layout params for temp if we are not // attaching. (If we are, we use addView, below) temp.setLayoutParams(params); } } ...... // Inflate all children under temp rInflate(parser, temp, attrs); ...... // We are supposed to attach all the views we found (int temp) // to root. Do that now. if (root != null && attachToRoot) { root.addView(temp, params); } // Decide whether to return the root that was passed in or the // top view found in xml. if (root == null || !attachToRoot) { result = temp; } } } catch (XmlPullParserException e) { ...... } catch (IOException e) { ...... } finally { ...... } return result; } } ...... }
這個函數定義在文件frameworks/base/core/java/android/view/LayoutInflater.java中。
LayoutInflater類的成員函數inflate主要負責處理前面所打開的Xml資源文件的根節點,然後再調用另外一個成員函數rInflate來處理根節點的子節點。每一個節點都表示一個UI控件,這個UI控件是通過調用LayoutInflater類的成員函數createViewFromTag來創建的。
LayoutInflater類的成員函數createViewFromTag需要兩個參數來創建一個UI控件。這兩個參數分別對應於當前正在處理的Xml節點的名稱以及屬性集。注意,如果參數root的值不等於null,那麼它所描述的一個ViewGroup就是調用LayoutInflater類的成員函數createViewFromTag獲得的UI控件的父控件。因此,調用LayoutInflater類的成員函數createViewFromTag獲得的UI控件及其對應的佈局參數,最後都需要添加到參數root所描述的一個ViewGroup中去。
有一種特殊情況,如果當前正在處理的Xml節點的名稱等於TAG_MERGE,即“merge”,那麼就表示不用處理的當前正在處理的Xml節點,而是直接去處理當前正在處理的Xml節點的子節點。這是Android系統爲提供的一種UI佈局優化機制,實際上就是減少了一層UI嵌套,具體可以參考官方文檔:http://developer.android.com/training/improving-layouts/reusing-layouts.html。
LayoutInflater類的成員函數rInflate在處理當前節點的子節點的時候,也是通過調用成員函數createViewFromTag來創建相應的UI控件的,因此,接下來我們就主要分析LayoutInflater類的成員函數createViewFromTag的實現。
Step 20. LayoutInflater.createViewFromTag
public abstract class LayoutInflater { ...... private Factory mFactory; ...... View createViewFromTag(String name, AttributeSet attrs) { if (name.equals("view")) { name = attrs.getAttributeValue(null, "class"); } ...... try { View view = (mFactory == null) ? null : mFactory.onCreateView(name, mContext, attrs); if (view == null) { if (-1 == name.indexOf('.')) { view = onCreateView(name, attrs); } else { view = createView(name, null, attrs); } } ...... return view; } catch (InflateException e) { ...... } catch (ClassNotFoundException e) { ...... } catch (Exception e) { ...... } } ...... }
這個函數定義在文件frameworks/base/core/java/android/view/LayoutInflater.java中。
參數name表示當前正在處理的Xml節點的名稱。如果它的值等於“view”的話,那麼真正要創建的UI控件的類名記錄在參數attrs所描述的一個屬性集中的一個名稱爲“class”的屬性中。因此,當參數name的值等於“view”的時候,LayoutInflater類的成員函數createViewFromTag首先要做的便是從參數attrs所描述的一個屬性集中獲取接下來真正要創建的UI控件的類名。
LayoutInflater類的成員函數createViewFromTag接下來檢查成員變量mFactory的值是否不等於null。如果不等於null的話,那麼它就會指向一個Factory對象,該Factory對象描述的是一個UI控件創建工廠,專門用來負責創建UI控件。
如果LayoutInflater類的成員變量mFactory的值等於null,那麼LayoutInflater類的成員函數createViewFromTag就會調用成員函數onCreateView或者createView來創建由參數name所指定的UI控件,取決於參數name是否包含了一個“.”字符。注意,如果參數name是否包含了一個“.”字符,那麼就說明當前所創建的UI控件是一個用戶自定義的UI控件,也就是不是Android提供的標準控件。
我們假設LayoutInflater類的成員變量mFactory的值等於null,並且參數name沒有包含有“.”字符,那麼LayoutInflater類的成員函數createViewFromTag最後就會調用成員函數onCreateView來創建由參數name所指定的UI控件。
LayoutInflater類的成員函數onCreateView是由其子類來重寫的。從前面的Step 2可以知道,當前正在處理的實際上是一個PhoneLayoutInflater對象。PhoneLayoutInflater類繼承了LayoutInflater類,並且重寫了成員函數onCreateView。因此,接下來我們就繼續分析PhoneLayoutInflater類的成員函數onCreateView的實現。
Step 21. PhoneLayoutInflater.onCreateView
public class PhoneLayoutInflater extends LayoutInflater { private static final String[] sClassPrefixList = { "android.widget.", "android.webkit." }; @Override protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException { for (String prefix : sClassPrefixList) { try { View view = createView(name, prefix, attrs); if (view != null) { return view; } } catch (ClassNotFoundException e) { // In this case we want to let the base class take a crack // at it. } } return super.onCreateView(name, attrs); } ...... }
這個函數定義在文件frameworks/base/policy/src/com/android/internal/policy/impl/PhoneLayoutInflater.java中。
PhoneLayoutInflater類的成員函數onCreateView只負責創建兩類標準的UI控件,一種是屬於android.widget包的,另一種是屬於android.webkit包的,其中,優先創建android.widget包的UI控件。
如果參數name所描述的UI控件既不屬於android.widget包的,也不屬於android.webkit包的,那麼 PhoneLayoutInflater類的成員函數onCreateView就會將創建UI控件的操作交給父類來處理,即通過調用父類的成員函數onCreateView來創建。
如果參數name所描述的UI控件是屬於android.widget包或者android.webkit包的,那麼PhoneLayoutInflater類的成員函數onCreateView就會直接調用父類LayoutInflater的成員函數createView來創建參數name所描述的UI控件,因此,接下來我們就繼續分析LayoutInflater類的成員函數createView的實現。
Step 22. LayoutInflater.createView
public abstract class LayoutInflater { ...... private Filter mFilter; private final Object[] mConstructorArgs = new Object[2]; ...... private static final HashMap<String, Constructor> sConstructorMap = new HashMap<String, Constructor>(); private HashMap<String, Boolean> mFilterMap; ...... public final View createView(String name, String prefix, AttributeSet attrs) throws ClassNotFoundException, InflateException { Constructor constructor = sConstructorMap.get(name); Class clazz = null; try { if (constructor == null) { // Class not found in the cache, see if it's real, and try to add it clazz = mContext.getClassLoader().loadClass( prefix != null ? (prefix + name) : name); if (mFilter != null && clazz != null) { boolean allowed = mFilter.onLoadClass(clazz); if (!allowed) { failNotAllowed(name, prefix, attrs); } } constructor = clazz.getConstructor(mConstructorSignature); sConstructorMap.put(name, constructor); } else { // If we have a filter, apply it to cached constructor if (mFilter != null) { // Have we seen this name before? Boolean allowedState = mFilterMap.get(name); if (allowedState == null) { // New class -- remember whether it is allowed clazz = mContext.getClassLoader().loadClass( prefix != null ? (prefix + name) : name); boolean allowed = clazz != null && mFilter.onLoadClass(clazz); mFilterMap.put(name, allowed); if (!allowed) { failNotAllowed(name, prefix, attrs); } } else if (allowedState.equals(Boolean.FALSE)) { failNotAllowed(name, prefix, attrs); } } } Object[] args = mConstructorArgs; args[1] = attrs; return (View) constructor.newInstance(args); } catch (NoSuchMethodException e) { ...... } catch (ClassNotFoundException e) { ...... } catch (Exception e) { ...... } } ...... }
這個函數定義在文件frameworks/base/core/java/android/view/LayoutInflater.java。
LayoutInflater類的靜態成員變量sConstructorMap指向的是一個HashMap。這個HashMap緩存了當前應用程序使用過的每一種類型的UI控件類的構造函數。這些構造函數必須具有兩個參數,其中第一個參數是一個Context,第二個參數是一個AttributeSet。LayoutInflater類的成員函數createView在創建一個UI控件的時候,就會將上述兩個參數保存在LayoutInflater類的成員變量mConstructorArgs所描述的一個大小爲2的數組中,並且以這個數組爲參數,來調用對應的UI控件類的構造函數。
LayoutInflater類的mFilter指向的是一個Filter對象。這個Filter對象描述的是一個過濾器,LayoutInflater類的成員函數createView在創建一個UI控件之前,會先調用該過濾器的成員函數onLoadClass,來詢問該過濾器是允許創建由參數name所描述的UI控件。如果不允許的話,LayoutInflater類的成員函數createView就會調用另外一個成員函數failNotAllowed來拋出一個異常。
爲了避免每次創建一個UI控件時,都去詢問過濾器是否允許創建,LayoutInflater類的成員函數createView只會在第一次創建一個名稱爲name的UI控件時,纔會詢問過濾器,並且將詢問結果保存在成員變量mFilterMap所指向的一個HashMap。這樣當LayoutInflater類的成員函數createView以後再創建同名的UI控件時,就可以直接通過成員變量mFilterMap所指向的一個HashMap來知道該UI控件是否是允許創建的。
理解了LayoutInflater類的靜態成員變量sConstructorMap以及成員變量mConstructorArgs、mFilter和mFilterMap的含義之後,讀者就可以自己去理解LayoutInflater類的成員函數createView的實現了。這裏需要重複強調的一點就是,我們在自定義一個UI控件的時候,一定要提供一個具有兩個參數類型分別爲Context和AttributeSet的構造函數,否則的話,該自定義控件就不可以在UI佈局文件中使用。
至此,我們就以layout資源爲例,詳細地分析了Android應用程序資源的查找過程了,並且也完整地分析完成Android系統的資源管理框架了。重新學習Android系統的資源管理框架,請參考前面Android資源管理框架(Asset Manager)簡要介紹和學習計劃一文。
老羅的新浪微博:http://weibo.com/shengyangluo,歡迎關注!