天天看點

Android資源檔案的管理

android程式中,不管是普通的drawable、anim、color、layout、value等類型的資源檔案,還是raw類型的資源檔案,或者asset類型的資源檔案,所有的資源檔案都是通過AssetManager管理的。其中asset資源檔案,在打包apk時,不會有任何的改動,會被原封不動的打包進apk内,可以通過檔案名和路徑通路;而raw資源檔案打包時也會原封不動的打包,隻不過使用資源id通路;而其它類型的資源檔案,在打包的時候,可能會被優化(bitmap),使用資源id通路。

Android資源檔案的管理

以下,我們通過代碼分析資源檔案的管理。

1、Resources.java

位于目錄/frameworks/base/core/java/android/content/res/Resources.java。Resources的代碼邏輯很簡單,基本就是直接調用了AssetManager的方法。需要注意的是有2個方法startPreloading()和finishPreloading()。這兩個方法僅在ZygoteInit.java(Zygote程序)中調用,專門用于加載需要預加載的資源檔案(在com.android.internal.R.array.preloaded_drawables中定義),并将這些資源檔案存儲在Resources的全局變量中。在應用程序啟動(由Zygote程序fork)時,這些資料将從Zygote程序複制到應用程序中。

/**  
     * Start preloading of resource data using this Resources object.  Only
     * for use by the zygote process for loading common system resources.
     * {@hide}
     */
    public final void startPreloading() {
        synchronized (mSync) {
            if (sPreloaded) {
                throw new IllegalStateException("Resources already preloaded");
            }
            sPreloaded = true;
            mPreloading = true;
            sPreloadedDensity = DisplayMetrics.DENSITY_DEVICE;
            mConfiguration.densityDpi = sPreloadedDensity;

            // 最終會調用AssetManager.setConfiguration(),已經緩存的資料可能會被删除
            updateConfiguration(null, null);
        }
    }    

    /**  
     * Called by zygote when it is done preloading resources, to change back
     * to normal Resources operation.
     */
    public final void finishPreloading() {
        if (mPreloading) {
            mPreloading = false;
            flushLayoutCache(); // 重新整理資料,config不比對的資料将被移除
        }
    }

    public String getString(int id) throws NotFoundException {
        CharSequence res = getText(id);
        if (res != null) {
            return res.toString();
        }
        throw new NotFoundException("String resource ID #0x"
                                    + Integer.toHexString(id));
    }

    public CharSequence getText(int id) throws NotFoundException {
        // 調用了AssetManager的getResourceText()方法
        CharSequence res = mAssets.getResourceText(id);
        if (res != null) {
            return res; 
        }
        throw new NotFoundException("String resource ID #0x"
                                    + Integer.toHexString(id));
    }   
           

2、AssetManager.java

位于/frameworks/base/core/java/android/content/res/目錄。其内部通過一個StringBlock類管理所有string的資源。對應的還有一個XmlBlock類,管理所有的xml資源檔案。但是,XmlBlock卻是直接在Resource.java中緩存的,并且隻能緩存最近加載的4個,是出于記憶體占用的考慮麼???。另外,AssetManager還有一個比較重要的方法int addAssetPath(String path),用于導入zip資源檔案或者資源目錄。例如,可以使用該方法導入apk自帶資源檔案,或者導入theme apk等。在Activity初始化的時候,就是用了該方法導入的apk自帶資源檔案。有興趣的話,可以自行查閱ActivityThread.java檔案(搜尋getTopLevelResources()方法,該方法在ContextImpl.java對象init時調用)。

public AssetManager() {
        synchronized (this) {
            init(); // jni方法,建立native asset manager,并将其指針存放在mObject字段
            ensureSystemAssets();
        }
    }

    private static void ensureSystemAssets() {  // 建立system asset manager
        synchronized (sSync) {
            if (sSystem == null) {
                AssetManager system = new AssetManager(true);

                // 初始化system asset manager的StringBlocks
                system.makeStringBlocks(false);
                sSystem = system;
            }
        }
    }

    // 初始化system string blocks 或者 複制system string blocks. 
    /*package*/ final void makeStringBlocks(boolean copyFromSystem) {
        final int sysNum = copyFromSystem ? sSystem.mStringBlocks.length : ;
        final int num = getStringBlockCount();
        mStringBlocks = new StringBlock[num];
        if (localLOGV) Log.v(TAG, "Making string blocks for " + this
                + ": " + num);
        for (int i=; i<num; i++) {
            if (i < sysNum) {
                // 淺拷貝system string blocks
                mStringBlocks[i] = sSystem.mStringBlocks[i];
            } else {
                // getNativeStringBlock()是一個jni方法
                mStringBlocks[i] = new StringBlock(getNativeStringBlock(i), true);
            }
        }
    }

    /** 
     * Retrieve the string value associated with a particular resource
     * identifier for the current configuration / skin.
     */
    /*package*/ final CharSequence getResourceText(int ident) {
        synchronized (this) {
            TypedValue tmpValue = mValue;

            // jni方法,猜想:多個StringBlocks,每個StringBlocks管理一種類型的資源
            int block = loadResourceValue(ident, (short) , tmpValue, true);
            if (block >= ) {
                if (tmpValue.type == TypedValue.TYPE_STRING) {
                    return mStringBlocks[block].get(tmpValue.data);
                }
                return tmpValue.coerceToString();
            }
        }
        return null;
    }   

    /**
     * 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);
           

3、StringBlocks.java

位于/frameworks/base/core/java/android/content/res/目錄。該對象主要是從底層的一個類似數組的結構體中使用index擷取資料,并将資料緩存在java層的string數組或者SparseArray中。

// 該構造方法隻在system asset manager建立的時候調用
    // obj:對應底層的一個類似數組的結構體;useSparse:是否使用SparseArray緩存資料
    StringBlock(int obj, boolean useSparse) {
        mNative = obj;
        mUseSparse = useSparse;
        mOwnsNative = false;
    }

    public StringBlock(byte[] data, int offset, int size, boolean useSparse) {
        // nativeCreate()實際就是使用data、offset、size做淺複制,并将複制之後的一個結構體
        // 指針儲存在mNative字段中
        mNative = nativeCreate(data, offset, size);
        mUseSparse = useSparse;
        mOwnsNative = true;
    }

    public CharSequence get(int idx) {
        synchronized (this) {
            if (mStrings != null) {
                CharSequence res = mStrings[idx];
                if (res != null) {
                    return res;
                }
            } else if (mSparseStrings != null) {
                CharSequence res = mSparseStrings.get(idx);
                if (res != null) {
                    return res;
                }
            } else {
                final int num = nativeGetSize(mNative);
                if (mUseSparse && num > ) { // 大于250時使用SparseArray緩存
                    mSparseStrings = new SparseArray<CharSequence>();
                } else {
                    mStrings = new CharSequence[num];
                }
            }

            String str = nativeGetString(mNative, idx);
            CharSequence res = str;
            int[] style = nativeGetStyle(mNative, idx);

            if (style != null) {
                if (mStyleIDs == null) {
                    mStyleIDs = new StyleIDs();
                }

                // 。。。。省略,建構style ids
                String styleTag = nativeGetString(mNative, styleId);                                    
                res = applyStyles(str, style, mStyleIDs);
            }

            // 緩存資源
            if (mStrings != null) mStrings[idx] = res;
            else mSparseStrings.put(idx, res);
            return res;
           

4、android_util_AssetManager.cpp

位于/frameworks/base/core/jni/android_util_AssetManager.cpp。對接上層的AssetManager.java,邏輯不多,基本是直接調用到native AssetManager的方法。

static void android_content_AssetManager_init(JNIEnv* env, jobject clazz)
{
    AssetManager* am = new AssetManager();
    if (am == NULL) {
        jniThrowException(env, "java/lang/OutOfMemoryError", ""); 
        return;
    }    

    // 加載/framework/framework-res.apk資源
    am->addDefaultAssets();

    ALOGV("Created AssetManager %p for Java object %p\n", am, clazz);

    // 将native asset manager對象的指針存入java asset manager的mObject字段
    env->SetIntField(clazz, gAssetManagerOffsets.mObject, (jint)am);
}


// ident:資源id,density:0,outValue:TypedValue,resolve:true
static jint android_content_AssetManager_loadResourceValue(JNIEnv* env, jobject clazz,
                                                           jint ident,
                                                           jshort density,
                                                           jobject outValue,
                                                           jboolean resolve) 
{
    // 從java層的AssetManager對象的mObject字段中擷取native AssetManager對象。
    // 該字段在java AssetManager對象init時設定
    AssetManager* am = assetManagerForJavaObject(env, clazz);
    if (am == NULL) {
        return ;
    }    
    const ResTable& res(am->getResources());

    Res_value value;
    ResTable_config config;
    uint32_t typeSpecFlags;
    ssize_t block = res.getResource(ident, &value, false, density, &typeSpecFlags, &config);

    uint32_t ref = ident;
    if (resolve) {
        block = res.resolveReference(&value, block, &ref, &typeSpecFlags, &config);

    }

    // copyValue()将底層的Res_value對象的字段值複制到上層的TypedValue對象中    
    return block >=  ? copyValue(env, outValue, &res, value, ref, block, typeSpecFlags, &config) : block;
}

// 加載xml資源檔案
static jint android_content_AssetManager_openXmlAssetNative(JNIEnv* env, jobject clazz,
                                                         jint cookie,
                                                         jstring fileName)
{
    AssetManager* am = assetManagerForJavaObject(env, clazz);
    if (am == NULL) {
        return ;
    }

    ALOGV("openXmlAsset in %p (Java object %p)\n", am, clazz);

    ScopedUtfChars fileName8(env, fileName);
    if (fileName8.c_str() == NULL) {
        return ;
    }

    Asset* a = cookie
        ? am->openNonAsset((void*)cookie, fileName8.c_str(), Asset::ACCESS_BUFFER)
        : am->openNonAsset(fileName8.c_str(), Asset::ACCESS_BUFFER);

    if (a == NULL) {
        jniThrowException(env, "java/io/FileNotFoundException", fileName8.c_str());
        return ;
    }

    ResXMLTree* block = new ResXMLTree();
    status_t err = block->setTo(a->getBuffer(true), a->getLength(), true);
    a->close();
    delete a;

    if (err != NO_ERROR) {
        jniThrowException(env, "java/io/FileNotFoundException", "Corrupt XML binary file");
        return ;
    }

    return (jint)block;
}
           

5、AssetManager.cpp

位于/frameworks/base/libs/androidfw/目錄。

static const char* kDefaultLocale = "default";
static const char* kDefaultVendor = "default";
static const char* kAssetsRoot = "assets";
static const char* kAppZipName = NULL; //"classes.jar";
static const char* kSystemAssets = "framework/framework-res.apk";
static const char* kIdmapCacheDir = "resource-cache";

bool AssetManager::addDefaultAssets()
{
    const char* root = getenv("ANDROID_ROOT");  // 預設是/system路徑
    LOG_ALWAYS_FATAL_IF(root == NULL, "ANDROID_ROOT not set");

    String8 path(root);
    path.appendPath(kSystemAssets); // 絕對路徑/system/framework/framework-res.apk

    return addAssetPath(path, NULL);
}

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) {
            ALOGW("Asset path %s is neither a directory nor file (type=%d).",
                 path.string(), (int)ap.type);
            return false;
        }
    }

    // Skip if we have it already.
    // 從已加載的資源中查找同一cookie的資源檔案index,確定資源檔案不會被重複加載
    for (size_t i=; i<mAssetPaths.size(); i++) {
        if (mAssetPaths[i].path == ap.path) {
            if (cookie) {
                *cookie = (void*)(i+);
            }
            return true;
        }
    }

    ALOGV("In %p Asset %s path: %s", this,
         ap.type == kFileTypeDirectory ? "dir" : "zip", ap.path.string());

    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/", ) == ) { // 比對/system/framework
        // 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)) == ) {
            asset_path oap;
            oap.path = overlayPath;
            oap.type = ::getFileType(overlayPath.string());
            bool addOverlay = (oap.type == kFileTypeRegular); // only .apks supported as overlay
            if (addOverlay) {
                // id map檔案路徑:将overlayPath中的所有/轉換為@
                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);
            } else {
                ALOGW("failed to add overlay package %s\n", overlayPath.string());
            }
        }
    }

    return true;
}

// 加載xml資源檔案
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;
}
           

注意:AssetManager類的成員函數addAssetPath的最後一個工作是檢查剛剛添加的Apk是否是在/system/framework/目錄下面。如果是,就會在/vendor/overlay/framework/目錄下找到一個同名的Apk檔案,并且也會将該Apk檔案添加到成員變量mAssetPaths(Vector)中去。這是一種資源覆寫機制,即用自定義的系統資源來覆寫系統預設的系統資源,以達到個性化系統界面的目的。如果要使用這種資源覆寫機制來自定義自己的系統資源,那麼還需要提供一個idmap檔案,用來說明它在/vendor/overlay/framework/目錄下的Apk檔案要覆寫系統的哪些資源,使用資源ID來描述。是以,這個idmap檔案實際上就是一個資源ID映射檔案。這個idmap檔案最終儲存在/data/resource-cache/目錄下,并且按照一定的格式來命名。例如,如果apk為/vendor/overlay/framework/framework-res.apk,那麼對應的idmap檔案就會以名稱為@[email protected]@[email protected]@idmap。

另外:資源解析之後,在底層是存放在一個ResTable中的。當系統的config改變時,最終會通知到ResTable。ResTable會更新緩存資料。

6、總結

  • zygote程序初始化時,調用startPreloading()預加載了一部分系統資源
  • 應用程序由zygote程序fork啟動時,複制了這些資源檔案,并儲存在一個static system asset manager中的StringBlock數組中
  • 應用程序在ActivityThread中調用getTopLevelResources()加載了自身apk的資源檔案,存儲在底層資料結構ResTable中
  • 應用在查找string資源時,如果從StringBlock緩存中無法找到,則從底層ResTable中查找,之後在儲存在StringBlock中
  • 應用在查找xml資源時,如果從Resource.java的XmlBlock緩存中無法找到,則直接從檔案中查找,最終傳回XmlBlock的一個内部Parser