1. 程式人生 > >Android原始碼解析之應用程式資源管理器(Asset Manager)的建立過程分析

Android原始碼解析之應用程式資源管理器(Asset Manager)的建立過程分析

轉載自:https://blog.csdn.net/luoshengyang/article/details/8791064

我們分析了Android應用程式資源的編譯和打包過程,最終得到的應用程式資源就與應用程式程式碼一起打包在一個APK檔案中。Android應用程式在執行的過程中,是通過一個稱為AssetManager的資源管理器來讀取打包在APK檔案裡面的資原始檔的。在本文中,我們就將詳細分析Android應用程式資源管理器的建立以及初始化過程,為接下來的一篇文章分析應用程式資源的讀取過程打下基礎。

        從前面Android應用程式視窗(Activity)的執行上下文環境(Context)的建立過程分析

一文可以知道,應用程式的每一個Activity元件都關聯有一個ContextImpl物件,這個ContextImpl物件就是用來描述Activity元件的執行上下文環境的。Activity元件是從Context類繼承下來的,而ContextImpl同樣是從Context類繼承下來的。我們在Activity元件呼叫的大部分成員函式都是轉發給與它所關聯的一個ContextImpl物件的對應的成員函式來處理的,其中就包括用來訪問應用程式資源的兩個成員函式getResources和getAssets。

        ContextImpl類的成員函式getResources返回的是一個Resources物件,有了這個Resources物件之後,我們就可以通過資源ID來訪問那些被編譯過的應用程式資源了。ContextImpl類的成員函式getAssets返回的是一個AssetManager物件,有了這個AssetManager物件之後,我們就可以通過檔名來訪問那些被編譯過或者沒有被編譯過的應用程式資原始檔了。事實上,Resources類也是通過AssetManager類來訪問那些被編譯過的應用程式資原始檔的,不過在訪問之前,它會先根據資源ID查詢得到對應的資原始檔名。

        我們知道,在Android系統中,一個程序是可以同時載入多個應用程式的,也就是可以同時載入多個APK檔案。每一個APK檔案在程序中都對應有一個全域性的Resourses物件以及一個全域性的AssetManager物件。其中,這個全域性的Resourses物件儲存在一個對應的ContextImpl物件的成員變數mResources中,而這個全域性的AssetManager物件儲存在這個全域性的Resourses物件的成員變數mAssets中。上述ContextImpl、Resourses和AssetManager的關係如圖1所示:

圖1 ContextImpl、Resources和AssetManager的關係圖

        Resources類有一個成員函式getAssets,通過它就可以獲得儲存在Resources類的成員變數mAssets中的AssetManager,例如,ContextImpl類的成員函式getAssets就是通過呼叫其成員變數mResources所指向的一個Resources物件的成員函式getAssets來獲得一個可以用來訪問應用程式的非編譯資原始檔的AssetManager。

        我們知道,Android應用程式除了要訪問自己的資源之外,還需要訪問系統的資源。系統的資源打包在/system/framework/framework-res.apk檔案中,它在應用程式程序中是通過一個單獨的Resources物件和一個單獨的AssetManager物件來管理的。這個單獨的Resources物件就儲存在Resources類的靜態成員變數mSystem中,我們可以通過Resources類的靜態成員函式getSystem就可以獲得這個Resources物件,而這個單獨的AssetManager物件就儲存在AssetManager類的靜態成員變數sSystem中,我們可以通過AssetManager類的靜態成員函式getSystem同樣可以獲得這個AssetManager物件。

        AssetManager類除了在Java層有一個實現之外,在 C++層也有一個對應的實現,而Java層的AssetManager類的功能就是通過C++層的AssetManager類來實現的。Java層的每一個AssetManager物件都有一個型別為int的成員變數mObject,它儲存的便是在C++層對應的AssetManager物件的地址,因此,通過這個成員變數就可以將Java層的AssetManager物件與C++層的AssetManager物件關聯起來。

        C++層的AssetManager類有三個重要的成員變數mAssetPaths、mResources和mConfig。其中,mAssetPaths儲存的是資源存放目錄,mResources指向的是一個資源索引表,而mConfig儲存的是裝置的本地配置資訊,例如螢幕密度和大小、國家地區和語言等等配置資訊。有了這三個成員變數之後,C++層的AssetManager類就可以訪問應用程式的資源了。

        從前面Android應用程式啟動過程原始碼分析一文可以知道,每一個Activity元件在程序的載入過程中,都會建立一個對應的ContextImpl,並且呼叫這個ContextImpl物件的成員函式init來執行初始化Activity元件執行上下文環境的工作,其中就包括建立用來訪問應用程式資源的Resources物件和AssetManager物件的工作,接下來,我們就從ContextImpl類的成員函式init開始分析Resources物件和AssetManager物件的建立以及初始化過程,如圖2所示:

圖2 應用程式資源管理器的建立和初始化過程

        這個過程可以分為14個步驟,接下來我們就詳細分析每一個步驟。

        Step 1. ContextImpl.init

class ContextImpl extends Context {
    ......
 
    /*package*/ LoadedApk mPackageInfo;
    private Resources mResources;
    ......
 
    final void init(LoadedApk packageInfo,
            IBinder activityToken, ActivityThread mainThread) {
        init(packageInfo, activityToken, mainThread, null);
    }
 
    final void init(LoadedApk packageInfo,
                IBinder activityToken, ActivityThread mainThread,
                Resources container) {
        mPackageInfo = packageInfo;
        mResources = mPackageInfo.getResources(mainThread);
 
        ......
    }
 
    ......
}

 

        這個函式定義在檔案frameworks/base/core/java/android/app/ContextImpl.java中。

        引數packageInfo指向的是一個LoadedApk物件,這個LoadedApk物件描述的是當前正在啟動的Activity組所屬的Apk。三個引數版本的成員函式init呼叫了四個引數版本的成員函式init來初始化當前正在啟動的Activity元件的執行上下文環境。其中,用來訪問應用程式資源的Resources物件是通過呼叫引數packageInfo所指向的是一個LoadedApk物件的成員函式getResources來建立的。這個Resources物件建立完成之後,就會儲存在ContextImpl類的成員變數mResources中。

        接下來,我們就繼續分析LoadedApk類的成員函式getResources的實現。

        Step 2. LoadedApk.getResources

final class LoadedApk {
    ......
 
    private final String mResDir;
    ......
 
    Resources mResources;
    ......
 
    public Resources getResources(ActivityThread mainThread) {
        if (mResources == null) {
            mResources = mainThread.getTopLevelResources(mResDir, this);
        }
        return mResources;
    }
 
    ......
}

        這個函式定義在檔案frameworks/base/core/java/android/app/LoadedApk.java中。

 

        引數mainThread指向了一個ActivityThread物件,這個ActivityThread物件描述的是當前正在執行的應用程式程序。

        LoadedApk類的成員函式getResources首先檢查其成員變數mResources的值是否等於null。如果不等於的話,那麼就會將它所指向的是一個Resources物件返回給呼叫者,否則的話,就會呼叫引數mainThread所指向的一個ActivityThread物件的成員函式getTopLevelResources來獲得這個Resources物件,然後再返回給呼叫者。

        在呼叫ActivityThread類的成員函式getTopLevelResources來獲得一個Resources物件的時候,需要指定要獲取的Resources物件所對應的Apk檔案路徑,這個Apk檔案路徑就儲存在LoadedApk類的成員變數mResDir中。例如,假設我們要獲取的Resources物件是用來訪問系統自帶的音樂播放器的資源的,那麼對應的Apk檔案路徑就為/system/app/Music.apk。

        接下來,我們就繼續分析ActivityThread類的成員函式getTopLevelResources的實現。

        Step 3. ActivityThread.getTopLevelResources

public final class ActivityThread {
    ......
 
    final HashMap<ResourcesKey, WeakReference<Resources> > mActiveResources
            = new HashMap<ResourcesKey, WeakReference<Resources> >();
    ......
 
    Resources getTopLevelResources(String resDir, CompatibilityInfo compInfo) {
        ResourcesKey key = new ResourcesKey(resDir, compInfo.applicationScale);
        Resources r;
        synchronized (mPackages) {
            ......
 
            WeakReference<Resources> wr = mActiveResources.get(key);
            r = wr != null ? wr.get() : null;
            ......
 
            if (r != null && r.getAssets().isUpToDate()) {
                ......
                return r;
            }
        }
 
        ......
 
        AssetManager assets = new AssetManager();
        if (assets.addAssetPath(resDir) == 0) {
            return null;
        }
        ......
 
        r = new Resources(assets, metrics, getConfiguration(), compInfo);
        ......
 
        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;
        }
    }
 
    ......
}

        這個函式定義在檔案frameworks/base/core/java/android/app/ActivityThread.java中。

 

        ActivityThread類的成員變數mActiveResources指向的是一個HashMap。這個HashMap用來維護在當前應用程式程序中載入的每一個Apk檔案及其對應的Resources物件的對應關係。也就是說,給定一個Apk檔案路徑,ActivityThread類的成員函式getTopLevelResources可以在成員變數mActiveResources中檢查是否存在一個對應的Resources物件。如果不存在,那麼就會新建一個,並且儲存在ActivityThread類的成員變數mActiveResources中。

        引數resDir即為要獲取其對應的Resources物件的Apk檔案路徑,ActivityThread類的成員函式getTopLevelResources首先根據它來建立一個ResourcesKey物件,然後再以這個ResourcesKey物件在ActivityThread類的成員變數mActiveResources中檢查是否存在一個Resources物件。如果存在,並且這個Resources物件裡面包含的資原始檔沒有過時,即呼叫這個Resources物件的成員函式getAssets所獲得一個AssetManager物件的成員函式isUpToDate的返回值等於true,那麼ActivityThread類的成員函式getTopLevelResources就可以將該Resources物件返回給呼叫者了。

        如果不存在與引數resDir對應的Resources物件,或者存在這個Resources物件,但是存在的這個Resources物件是過時的,那麼ActivityThread類的成員函式getTopLevelResources就會新建立一個AssetManager物件,並且呼叫這個新建立的AssetManager物件的成員函式addAssetPath來將引數resDir所描述的Apk檔案路徑作為它的資源目錄。

        建立了一個新的AssetManager物件,ActivityThread類的成員函式getTopLevelResources還需要這個AssetManager物件來建立一個新的Resources物件。這個新建立的Resources物件需要以前面所建立的ResourcesKey物件為鍵值快取在ActivityThread類的成員變數mActiveResources所描述的一個HashMap中,以便以後可以獲取回來使用。不過,這個新建立的Resources物件在快取到ActivityThread類的成員變數mActiveResources所描述的一個HashMap去之前,需要再次檢查該HashMap是否已經存在一個對應的Resources物件了,這是因為當前執行緒在建立新的AssetManager物件和Resources物件的過程中,可能有其它執行緒搶先一步建立了與引數resDir對應的Resources物件,並且將該Resources物件儲存到該HashMap中去了。

        如果沒有其它執行緒搶先建立一個與引數resDir對應的Resources物件,或者其它執行緒搶先創建出來的Resources物件是過時的,那麼ActivityThread類的成員函式getTopLevelResources就會將前面建立的Resources物件快取到成員變數mActiveResources所描述的一個HashMap中去,並且將前面建立的Resources物件返回給呼叫者,否則擴知,就會將其它執行緒搶先建立的Resources物件返回給呼叫者。

        接下來,我們首先分析AssetManager類的建構函式和成員函式addAssetPath的實現,接著再分析Resources類的建構函式的實現,以便可以瞭解用來訪問應用程式資源的AssetManager物件和Resources物件的建立以及初始化過程。

        Step 4. new AssetManager

public final class AssetManager {
    ......
 
    private static AssetManager sSystem = null;
    ......
 
    public AssetManager() {
        synchronized (this) {
            ......
            init();
            ......
            ensureSystemAssets();
        }
    }
 
    private static void ensureSystemAssets() {
        synchronized (sSync) {
            if (sSystem == null) {
                AssetManager system = new AssetManager(true);
                system.makeStringBlocks(false);
                sSystem = system;
            }
        }
    }
 
    ......
}

        這個函式定義在檔案frameworks/base/core/java/android/content/res/AssetManager.java中。

 

        AssetManager類的建構函式是通過呼叫另外一個成員函式init來執行初始化工作的。在初始化完成當前正在建立的AssetManager物件之後,AssetManager類的建構函式還會呼叫另外一個成員函式ensureSystemAssets來檢查當前程序是否已經建立了一個用來訪問系統資源的AssetManager物件。

        如果用來訪問系統資源的AssetManager物件還沒有建立的話,那麼AssetManager類的成員函式ensureSystemAssets就會建立並且初始化它,並且將它儲存在AssetManager類的靜態成員變數sSystem中。注意,建立用來訪問系統資源和應用程式資源的AssetManager物件的過程是一樣的,區別只在於它們所要訪問的Apk檔案不一樣,因此,接下來我們就只分析用來訪問應用資源的AssetManager物件的建立過程以及初始化過程。

       Step 5. AssetManager.init

public final class AssetManager {
    ......
 
    private int mObject;
    ......
 
    private native final void init();
 
    ......
}

 

       這個函式定義在檔案frameworks/base/core/java/android/content/res/AssetManager.java中。

       AssetManager類的成員函式init是一個JNI函式,它是由C++層的函式android_content_AssetManager_init來實現的:

static void android_content_AssetManager_init(JNIEnv* env, jobject clazz)
{
    AssetManager* am = new AssetManager();
    .....
 
    am->addDefaultAssets();
 
    ......
    env->SetIntField(clazz, gAssetManagerOffsets.mObject, (jint)am);
}

         這個函式定義在檔案frameworks/base/core/jni/android_util_AssetManager.cpp中。

 

         函式android_content_AssetManager_init首先建立一個C++層的AssetManager物件,接著呼叫這個C++層的AssetManager物件的成員函式addDefaultAssets來新增預設的資源路徑,最後將這個這個C++層的AssetManager物件的地址儲存在引數clazz所描述的一個Java層的AssetManager物件的成員變數mObject中。

        Step 6. AssetManager.addDefaultAssets

static const char* kSystemAssets = "framework/framework-res.apk";
......
 
bool AssetManager::addDefaultAssets()
{
    const char* root = getenv("ANDROID_ROOT");
    ......
 
    String8 path(root);
    path.appendPath(kSystemAssets);
 
    return addAssetPath(path, NULL);
}

       這個函式定義在檔案frameworks/base/libs/utils/AssetManager.cpp中。

 

       AssetManager類的成員函式addDefaultAssets首先通過環境變數ANDROID_ROOT來獲得Android的系統路徑,接著再將全域性變數kSystemAssets所指向的字串“framework/framework-res.apk”附加到這個系統路徑的後面去,這樣就可以得到系統資原始檔framework-res.apk的絕對路徑了。一般來說,環境變數ANDROID_ROOT所設定的Android系統路徑就是“/system”,因此,最終得到的系統資原始檔的絕對路徑就為“/system/framework/framework-res.apk”。

       得到了系統資原始檔framework-res.apk的絕對路徑之後,就呼叫AssetManager類的成員函式addAssetPath來將它新增到當前正在初始化的AssetManager物件中去。

       Step 7. AssetManager.addAssetPath

static const char* kAppZipName = NULL; //"classes.jar";
......
 
bool AssetManager::addAssetPath(const String8& path, void** cookie)
{
    AutoMutex _l(mLock);
 
    asset_path ap;
 
    String8 realPath(path);
    if (kAppZipName) {
        realPath.appendPath(kAppZipName);
    }
    ap.type = ::getFileType(realPath.string());
    if (ap.type == kFileTypeRegular) {
        ap.path = realPath;
    } else {
        ap.path = path;
        ap.type = ::getFileType(path.string());
        if (ap.type != kFileTypeDirectory && ap.type != kFileTypeRegular) {
            ......
            return false;
        }
    }
 
    // Skip if we have it already.
    for (size_t i=0; i<mAssetPaths.size(); i++) {
        if (mAssetPaths[i].path == ap.path) {
            if (cookie) {
                *cookie = (void*)(i+1);
            }
            return true;
        }
    }
 
    ......
 
    mAssetPaths.add(ap);
 
    // new paths are always added at the end
    if (cookie) {
        *cookie = (void*)mAssetPaths.size();
    }
 
    // add overlay packages for /system/framework; apps are handled by the
    // (Java) package manager
    if (strncmp(path.string(), "/system/framework/", 18) == 0) {
        // When there is an environment variable for /vendor, this
        // should be changed to something similar to how ANDROID_ROOT
        // and ANDROID_DATA are used in this file.
        String8 overlayPath("/vendor/overlay/framework/");
        overlayPath.append(path.getPathLeaf());
        if (TEMP_FAILURE_RETRY(access(overlayPath.string(), R_OK)) == 0) {
            asset_path oap;
            oap.path = overlayPath;
            oap.type = ::getFileType(overlayPath.string());
            bool addOverlay = (oap.type == kFileTypeRegular); // only .apks supported as overlay
            if (addOverlay) {
                oap.idmap = idmapPathForPackagePath(overlayPath);
 
                if (isIdmapStaleLocked(ap.path, oap.path, oap.idmap)) {
                    addOverlay = createIdmapFileLocked(ap.path, oap.path, oap.idmap);
                }
            }
            if (addOverlay) {
                mAssetPaths.add(oap);
            } 
            ......
        }
    }
 
    return true;
}

        這個函式定義在檔案frameworks/base/libs/utils/AssetManager.cpp中。

 

        如果全域性變數kAppZipName的值不等於NULL的話,那麼它的值一般就是被設定為“classes.jar”,這時候就表示應用程式的資原始檔是儲存在引數path所描述的一個目錄下的一個classes.jar檔案中。全域性變數kAppZipName的值一般被設定為NULL,並且引數path指向的是一個Apk檔案,因此,接下來我們只考慮應用程式資源不是儲存在一個classes.jar檔案的情況。

        AssetManager類的成員函式addAssetPath首先是要檢查引數path指向的是一個檔案或者目錄,並且該檔案或者目錄存在,否則的話,它就會直接返回一個false值,而不會再繼續往下處理了。

        AssetManager類的成員函式addAssetPath接著再檢查在其成員變數mAssetPaths所描述的一個型別為asset_path的Vector中是否已經新增過引數path所描述的一個Apk檔案路徑了。如果已經新增過了,那麼AssetManager類的成員函式addAssetPath就不會再繼續往下處理了,而是將與引數path所描述的一個Apk檔案路徑所對應的一個Cookie返回給呼叫者,即儲存在輸出引數cookie中,前提是引數cookie的值不等於NULL。一個Apk檔案路徑所對應的Cookie實際上只是一個整數,這個整數表示該Apk檔案路徑所對應的一個asset_path物件在成員變數mAssetPaths所描述的一個Vector中的索引再加上1。

        經過上面的檢查之後,AssetManager類的成員函式addAssetPath確保引數path所描述的一個Apk檔案路徑之前沒有被新增過,於是接下來就會將與該Apk檔案路徑所對應的一個asset_path物件儲存在成員變數mAssetPaths所描述的一個Vector的最末尾位置上,並且將這時候得到的Vector的大小作為一個Cookie值儲存在輸出引數cookie中返回給呼叫者。

        AssetManager類的成員函式addAssetPath的最後一個工作是檢查剛剛新增的Apk檔案路徑是否是儲存在/system/framework/目錄下面的。如果是的話,那麼就會在/vendor/overlay/framework/目錄下找到一個同名的Apk檔案,並且也會將該Apk檔案新增到成員變數mAssetPaths所描述的一個Vector中去。這是一種資源覆蓋機制,手機廠商可以利用它來自定義的系統資源,即用自定義的系統資源來覆蓋系統預設的系統資源,以達到個性化系統介面的目的。

        如果手機廠商要利用上述的資源覆蓋機制來自定義自己的系統資源,那麼還需要提供一個idmap檔案,用來說明它在/vendor/overlay/framework/目錄提供的Apk檔案要覆蓋系統的哪些預設資源,使用資源ID來描述,因此,這個idmap檔案實際上就是一個資源ID對映檔案。這個idmap檔案最終儲存在/data/resource-cache/目錄下,並且按照一定的格式來命令,例如,假設手機廠商提供的覆蓋資原始檔為/vendor/overlay/framework/framework-res.apk,那麼對應的idmap檔案就會以名稱為@[email protected]@[email protected]@idmap的形式儲存在目錄/data/resource-cache/下。

        關於Android系統的資源覆蓋(Overlay)機制,可以參考frameworks/base/libs/utils目錄下的READ檔案。

        這一步執行完成之後,回到前面的Step 3中,即ActivityThread類的成員函式getTopLevelResources中,接下來它就會呼叫前面所建立的Java層的AssetManager物件的成員函式addAssetPath來新增指定的應用程式資原始檔路徑。

        Step 8. AssetManager.addAssetPath

public final class AssetManager {
    ......
 
    /**
     * Add an additional set of assets to the asset manager.  This can be
     * either a directory or ZIP file.  Not for use by applications.  Returns
     * the cookie of the added asset, or 0 on failure.
     * {@hide}
     */
    public native final int addAssetPath(String path);
 
    ......
}

        這個函式定義在檔案frameworks/base/core/java/android/content/res/AssetManager.java中。

        AssetManager類的成員函式addAssetPath是一個JNI方法,它是由C++層的函式android_content_AssetManager_addAssetPath來實現的,如下所示:

static jint android_content_AssetManager_addAssetPath(JNIEnv* env, jobject clazz,
                                                       jstring path)
{
    ......
 
    AssetManager* am = assetManagerForJavaObject(env, clazz);
    ......
 
    const char* path8 = env->GetStringUTFChars(path, NULL);
 
    void* cookie;
    bool res = am->addAssetPath(String8(path8), &cookie);
 
    env->ReleaseStringUTFChars(path, path8);
 
    return (res) ? (jint)cookie : 0;
}

        這個函式定義在檔案frameworks/base/core/jni/android_util_AssetManager.cpp中。

 

        引數clazz指向的是Java層的一個AssetManager物件,函式android_content_AssetManager_addAssetPath首先呼叫另外一個函式assetManagerForJavaObject來將它的成員函式mObject轉換為一個C++層的AssetManager物件。有了這個C++層的AssetManager物件之後,就可以呼叫它的成員函式addAssetPath來將引數path所描述的一個Apk檔案路徑新增到它裡面去了,這個過程可以參考前面的Step 7。

        這一步執行完成之後,回到前面的Step 3中,即ActivityThread類的成員函式getTopLevelResources中,接下來就會根據前面所建立的Java層的AssetManager物件來建立一個Resources物件。

        Step 9. new Resources

public class Resources {
    ......
 
    /*package*/ final AssetManager mAssets;
    ......
 
    public Resources(AssetManager assets, DisplayMetrics metrics,
            Configuration config, CompatibilityInfo compInfo) {
        mAssets = assets;
        ......
 
        updateConfiguration(config, metrics);
        assets.ensureStringBlocks();
    }
 
    ......
}

        這個函式定義在檔案frameworks/base/core/java/android/content/res/Resources.java中。

 

        Resources類的建構函式首先將引數assets所指向的一個AssetManager物件儲存在成員變數mAssets中,以便以後可以通過它來訪問應用程式的資源,接下來呼叫另外一個成員函式updateConfiguration來設定裝置配置資訊,最後呼叫引數assets所指向的一個AssetManager物件的成員函式ensureStringBlocks來建立字串資源池。

        接下來,我們就首先分析Resources類的成員函式updateConfiguration的實現,接著再分析AssetManager類的成員函式ensureStringBlocks的實現。

        Step 10. Resources.updateConfiguration

public class Resources {
    ......
 
    private final Configuration mConfiguration = new Configuration();
    ......
 
    public void updateConfiguration(Configuration config,
            DisplayMetrics metrics) {
        synchronized (mTmpValue) {
            int configChanges = 0xfffffff;
            if (config != null) {
                configChanges = mConfiguration.updateFrom(config);
            }
            if (mConfiguration.locale == null) {
                mConfiguration.locale = Locale.getDefault();
            }
            if (metrics != null) {
                mMetrics.setTo(metrics);
                mMetrics.updateMetrics(mCompatibilityInfo,
                        mConfiguration.orientation, mConfiguration.screenLayout);
            }
            mMetrics.scaledDensity = mMetrics.density * mConfiguration.fontScale;
 
            String locale = null;
            if (mConfiguration.locale != null) {
                locale = mConfiguration.locale.getLanguage();
                if (mConfiguration.locale.getCountry() != null) {
                    locale += "-" + mConfiguration.locale.getCountry();
                }
            }
            int width, height;
            if (mMetrics.widthPixels >= mMetrics.heightPixels) {
                width = mMetrics.widthPixels;
                height = mMetrics.heightPixels;
            } else {
                //noinspection SuspiciousNameCombination
                width = mMetrics.heightPixels;
                //noinspection SuspiciousNameCombination
                height = mMetrics.widthPixels;
            }
            int keyboardHidden = mConfiguration.keyboardHidden;
            if (keyboardHidden == Configuration.KEYBOARDHIDDEN_NO
                    && mConfiguration.hardKeyboardHidden
                            == Configuration.HARDKEYBOARDHIDDEN_YES) {
                keyboardHidden = Configuration.KEYBOARDHIDDEN_SOFT;
            }
            mAssets.setConfiguration(mConfiguration.mcc, mConfiguration.mnc,
                    locale, mConfiguration.orientation,
                    mConfiguration.touchscreen,
                    (int)(mMetrics.density*160), mConfiguration.keyboard,
                    keyboardHidden, mConfiguration.navigation, width, height,
                    mConfiguration.screenLayout, mConfiguration.uiMode, sSdkVersion);
 
            ......
        }
       
        ...... 
    }
 
    ......
}

        這個函式定義在檔案frameworks/base/core/java/android/content/res/Resources.java中。

 

        Resources類的成員變數mConfiguration指向的是一個Configuration物件,用來描述裝置當前的配置資訊,這些配置資訊對應的就是在前面Android資源管理框架(Asset Manager)簡要介紹和學習計劃一文中提到18個資源維度。

        Resources類的成員函式updateConfiguration首先是根據引數config和metrics來更新裝置的當前配置資訊,例如,螢幕大小和密碼、國家地區和語言、鍵盤配置情況等等,接著再呼叫成員變數mAssets所指向的一個Java層的AssetManager物件的成員函式setConfiguration來將這些配置資訊設定到與之關聯的C++層的AssetManager物件中去。

        接下來,我們就繼續分析AssetManager類的成員函式setConfiguration的實現,以便可以瞭解裝置配置資訊的設定過程。

        Step 11. AssetManager.setConfiguration

public final class AssetManager {
    ......
 
    /**
     * Change the configuation used when retrieving resources.  Not for use by
     * applications.
     * {@hide}
     */
    public native final void setConfiguration(int mcc, int mnc, String locale,
            int orientation, int touchscreen, int density, int keyboard,
            int keyboardHidden, int navigation, int screenWidth, int screenHeight,
            int screenLayout, int uiMode, int majorVersion);
 
    ......
}

        這個函式定義在檔案frameworks/base/core/java/android/content/res/AssetManager.java中。

 

        AssetManager類的成員函式setConfiguration是一個JNI方法,它是由C++層的函式android_content_AssetManager_setConfiguration來實現的,如下所示:

static void android_content_AssetManager_setConfiguration(JNIEnv* env, jobject clazz,
                                                          jint mcc, jint mnc,
                                                          jstring locale, jint orientation,
                                                          jint touchscreen, jint density,
                                                          jint keyboard, jint keyboardHidden,
                                                          jint navigation,
                                                          jint screenWidth, jint screenHeight,
                                                          jint screenLayout, jint uiMode,
                                                          jint sdkVersion)
{
    AssetManager* am = assetManagerForJavaObject(env, clazz);
    if (am == NULL) {
        return;
    }
 
    ResTable_config config;
    memset(&config, 0, sizeof(config));
 
    const char* locale8 = locale != NULL ? env->GetStringUTFChars(locale, NULL) : NULL;
 
    config.mcc = (uint16_t)mcc;
    config.mnc = (uint16_t)mnc;
    config.orientation = (uint8_t)orientation;
    config.touchscreen = (uint8_t)touchscreen;
    config.density = (uint16_t)density;
    config.keyboard = (uint8_t)keyboard;
    config.inputFlags = (uint8_t)keyboardHidden;
    config.navigation = (uint8_t)navigation;
    config.screenWidth = (uint16_t)screenWidth;
    config.screenHeight = (uint16_t)screenHeight;
    config.screenLayout = (uint8_t)screenLayout;
    config.uiMode = (uint8_t)uiMode;
    config.sdkVersion = (uint16_t)sdkVersion;
    config.minorVersion = 0;
    am->setConfiguration(config, locale8);
 
    if (locale != NULL) env->ReleaseStringUTFChars(locale, locale8);
}

        這個函式定義在檔案frameworks/base/core/jni/android_util_AssetManager.cpp中。

 

        引數clazz指向的是一個Java層的AssetManager物件,函式android_content_AssetManager_setConfiguration首先呼叫另外一個函式assetManagerForJavaObject將它的成員變數mObject轉換為一個C++層的AssetManager物件。

        函式android_content_AssetManager_setConfiguration接下來再根據其它引數來建立一個ResTable_config物件,這個ResTable_config物件就用來描述裝置的當前配置資訊。

        函式android_content_AssetManager_setConfiguration最後呼叫前面獲得C++層的AssetManager物件的成員函式setConfiguration來將前面建立的ResTable_config物件設定到它內部去,以便C++層的AssetManager物件可以根據裝置的當前配置資訊來找到最合適的資源。

        Step 12. AssetManager.setConfiguration

void AssetManager::setConfiguration(const ResTable_config& config, const char* locale)
{
    AutoMutex _l(mLock);
    *mConfig = config;
    if (locale) {
        setLocaleLocked(locale);
    } else if (config.language[0] != 0) {
        char spec[9];
        spec[0] = config.language[0];
        spec[1] = config.language[1];
        if (config.country[0] != 0) {
            spec[2] = '_';
            spec[3] = config.country[0];
            spec[4] = config.country[1];
            spec[5] = 0;
        } else {
            spec[3] = 0;
        }
        setLocaleLocked(spec);
    } else {
        updateResourceParamsLocked();
    }
}

        這個函式定義在檔案frameworks/base/libs/utils/AssetManager.cpp中。

 

        AssetManager類的成員變數mConfig指向的是一個ResTable_config物件,用來描述裝置的當前配置資訊,AssetManager類的成員函式setConfiguration首先將引數config所描述的裝置配置資訊拷貝到它裡面去。

        如果引數local的值不等於NULL,那麼它指向的字串就是用來描述裝置的國家、地區和語言資訊的,這時候AssetManager類的成員函式setConfiguration就會呼叫另外一個成員函式setLocalLocked來將它們設定到AssetManager類的另外一個成員變數mLocale中去。

        如果引數local的值等於NULL,並且引數config指向的一個ResTable_config物件包含了裝置的國家、地區和語言資訊,那麼AssetManager類的成員函式setConfiguration同樣會呼叫另外一個成員函式setLocalLocked來將它們設定到AssetManager類的另外一個成員變數mLocale中去。

        如果引數local的值等於NULL,並且引數config指向的一個ResTable_config物件沒有包含裝置的國家、地區和語言資訊,那麼就說明裝置的國家、地區和語言等資訊不需要更新,這時候AssetManager類的成員函式setConfiguration就會直接呼叫另外一個成員函式updateResourceParamsLocked來更新資源表中的裝置配置資訊。

        注意,AssetManager類的成員函式setLocalLocked來更新了成員變數mLocale的內容之後,同樣會呼叫另外一個成員函式updateResourceParamsLocked來更新資源表中的裝置配置資訊。

        AssetManager類的成員函式updateResourceParamsLocked的實現如下所示:

void AssetManager::updateResourceParamsLocked() const
{
    ResTable* res = mResources;
    if (!res) {
        return;
    }
 
    size_t llen = mLocale ? strlen(mLocale) : 0;
    mConfig->language[0] = 0;
    mConfig->language[1] = 0;
    mConfig->country[0] = 0;
    mConfig->country[1] = 0;
    if (llen >= 2) {
        mConfig->language[0] = mLocale[0];
        mConfig->language[1] = mLocale[1];
    }
    if (llen >= 5) {
        mConfig->country[0] = mLocale[3];
        mConfig->country[1] = mLocale[4];
    }
    mConfig->size = sizeof(*mConfig);
 
    res->setParameters(mConfig);
}

        這個函式定義在檔案frameworks/base/libs/utils/AssetManager.cpp中。

 

        AssetManager類的成員變數mResources指向的是一個ResTable物件,這個ResTable物件描述的就是一個資源索引表。Android應用程式的資源索引表的格式以及生成過程可以參考前面Android應用程式資源的編譯和打包過程分析一文。

        AssetManager類的成員函式updateResourceParamsLocked首先是將成員變數mLocale所描述的國家、地區和語言資訊更新到另外一個成員變數mConfig中去,接著再將成員變數mConfig所包含的裝置配置資訊設定到成員變數mResources所描述的一個資源索引表中去,這是通過呼叫成員變數mResources所指向的一個ResTable物件的成員函式setParameters來實現的。

        這一步執行完成之後,返回到前面的Step 9中,即Resources類的建構函式,接下來它就會呼叫AssetManager類的成員函式ensureStringBlocks來建立字串資源池。

        Step 13. AssetManager.ensureStringBlocks

public final class AssetManager {
    ......
 
    private StringBlock mStringBlocks[] = null;
    ......
 
    /*package*/ final void ensureStringBlocks() {
        if (mStringBlocks == null) {
            synchronized (this) {
                if (mStringBlocks == null) {
                    makeStringBlocks(true);
                }
            }
        }
    }
 
    ......
}

        這個函式定義在檔案frameworks/base/core/java/android/content/res/AssetManager.java中。

 

        AssetManager類的成員變數mStringBlocks指向的是一個StringBlock陣列,其中,每一個StringBlock物件都是用來描述一個字串資源池。從前面Android應用程式資源的編譯和打包過程分析一文可以知道,每一個資源表都包含有一個資源項值字串資源池,AssetManager類的成員變數mStringBlocks就是用來儲存所有的資源表中的資源項值字串資源池的。

        AssetManager類的成員函式ensureStringBlocks首先檢查成員變數mStringBlocks的值是否等於null。如果等於null的話,那麼就說明當前應用程式使用的資源表中的資源項值字串資源池還沒有讀取出來,這時候就會呼叫另外一個成員函式makeStringBlocks來進行讀取。

       Step 14. AssetManager.makeStringBlocks

public final class AssetManager {
    ......
 
    private final void makeStringBlocks(boolean copyFromSystem) {
        final int sysNum = copyFromSystem ? sSystem.mStringBlocks.length : 0;
        final int num = getStringBlockCount();
        mStringBlocks = new StringBlock[num];
        ......
        for (int i=0; i<num; i++) {
            if (i < sysNum) {
                mStringBlocks[i] = sSystem.mStringBlocks[i];
            } else {
                mStringBlocks[i] = new StringBlock(getNativeStringBlock(i), true);
            }
        }
    }
 
    ......
 
    private native final int getStringBlockCount();
    private native final int getNativeStringBlock(int block);
 
    ......
}

        這個函式定義在檔案frameworks/base/core/java/android/content/res/AssetManager.java中。

        引數copyFromSystem表示是否要將系統資源表裡面的資源項值字串資源池也一起拷貝到成員變數mStringBlokcs所描述的一個數組中去。如果它的值等於true的時候,那麼AssetManager就會首先獲得makeStringBlocks首先獲得系統資源表的個數sysNum,接著再獲得總的資源表個數num,這是通過呼叫JNI方法getStringBlockCount來實現的。注意,總的資源表個數num是包含了系統資源表的個數sysNum的。

        從前面的Step 4可以知道,用來訪問系統資源包的AssetManager物件就儲存在AssetManager類的靜態成員變數sSystem中,並且這個AssetManager物件是最先被建立以及初始化的。也就是說,當執行到這一步的時候,所有系統資源表的資源項值字串資源池已經讀取出來,它們就儲存在AssetManager類的靜態成員變數sSystem所描述的一個AssetManager物件的成員變數mStringBlocks中,因此,只將它們拷貝到當前正在處理的AssetManager物件的成員變數mStringBlokcs的前sysNum個位置上去就可以了。

        最後,AssetManager類的成員函式makeStringBlocks就呼叫另外一個JNI方法getNativeStringBlock來讀取剩餘的其它資源表的資源項值字串資源池,並且分別將它們封裝在一個StringBlock物件儲存在成員變數mStringBlokcs所描述的一個數組中。

        AssetManager類的JNI方法getNativeStringBlock實際上就是將每一個資源包裡面的resources.arsc檔案的資源項值字串資源池資料塊讀取出來,並且封裝在一個C++層的StringPool物件中,然後AssetManager類的成員函式makeStringBlocks再將該StringPool物件封裝成一個Java層的StringBlock中。關於資源表中的資源項值字串資源池的更多資訊,可以參考前面Android應用程式資源的編譯和打包過程分析一文。

        至此,我們就分析完成Android應用程式資源管理器的建立的初始化過程了,主要就是建立和初始化用來訪問應用程式資源的AssetManager物件和Resources物件,其中,初始化操作包括設定AssetManager物件的資原始檔路徑以及裝置配置資訊等。