天天看点

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