Android應用程序資源管理器(Asset Manager)的創建過程分析

       在前面一篇文章中,我們分析了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文件就會以名稱爲@vendor@overlay@[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來實現的,如下所示:


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/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對象的資源文件路徑以及設備配置信息等。有了這兩個初始化的AssetManager對象和Resources對象之後,在接下來的一篇文章中,我們就可以繼續分析應用程序資源的查找過程了,敬請關注!

老羅的新浪微博:http://weibo.com/shengyangluo,歡迎關注!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章