天天看点

Android资源管理框架-------之最简单的资源信息的获取(六)

        前文我们说过了Android资源索引表,也就是resources.arsc是如何一步一步加载到内存,然后保存在资源管理相关的数据结构中的。本文,我们说说,当我们去访问资源相关的信息时,Android是如何一步一步把结果从这些数据结构中提取出来,返回给我们的。其实,当我们调用

Resources.java

中的相关方法去获取资源的时候,大多数情况下,系统是要分两步走的:先获取资源相关的信息,再根据这个信息去创建或者查找(有缓存的情况)或者加载资源本身。当然,对于一些比较简单的资源,系统是可以省掉第二步,直接拿到资源本身的,比如boolean、integer、float、String等。我们就先从这些简单的资源开始分析。

Integer资源的获取

        Android中的Integer资源就是一个整数值,非常简单,假如我们的xml中是这样的

<?xml version="1.0" encoding="utf-8"?>

<resources>
    
    <integer name="max_lines">25</integer>

</resources>
           

        当我调用

Resources

getInteger(int id)

方法时,Android是如何找到我们想要的这个整数值25的呢?我们一步一步来看代码:

//frameworks/base/base/core/java/android/content/res/Resources.java
public int getInteger(int id) throws NotFoundException {
    synchronized (mAccessLock) {
        TypedValue value = mTmpValue;
        if (value == null) {
            mTmpValue = value = new TypedValue();
        }
        /**
         * 主要逻辑在这个方法里
         * id 自然是资源id
         * value 这里是输出参数,查寻的结果放在里面
         * true 表示如果得到的结果不是最终结果,而是另外一个资源的引用的话,我们也会解析引用,直到得到最终结果
         */
        getValue(id, value, true);
        /**
         * 默认是十进制的整数即TypedValue.TYPE_INT_DEC
         * 对应于底层Res_Value中的枚举类型TYPE_INT_DEC
         */
        if (value.type >= TypedValue.TYPE_FIRST_INT
            && value.type <= TypedValue.TYPE_LAST_INT) {
            //类型匹配再返回其具体值
            return value.data;
        }
        //如果没找到,或者虽然找到,但是类型不匹配,抛出异常
        throw new NotFoundException(
             "Resource ID #0x" + Integer.toHexString(id) + " type #0x"
             + Integer.toHexString(value.type) + " is not valid");
    }
}
           

getInteger

方法仅仅是对

getValue

方法的封装,添加了线程相关和类型检查异常处理相关的东西。

public void getValue(int id, TypedValue outValue, boolean resolveRefs)
            throws NotFoundException {
    boolean found = mAssets.getResourceValue(id, 0, outValue, resolveRefs);
    if (found) {
        return;
    }
    throw new NotFoundException("Resource ID #0x"
                                    + Integer.toHexString(id));
}
           

getValue

方法更简单,封装

AssetManager

,从这个角度来讲,

Resources

更多的职能是一个接口类的角色。

//frameworks/base/core/java/android/content/res/AssetManager.java
final boolean getResourceValue(int ident, int density,/* 0 */
                         TypedValue outValue, boolean resolveRefs)
{
    //主要逻辑在这里
    int block = loadResourceValue(ident, (short) density, outValue, resolveRefs);
    //只要找到了资源,block值都会大于等于0
    if (block >= 0) {
        /**
         * 我们这里的类型是TypedValue.TYPE_INT_DEC
         * 所以会直接返回
         */
        if (outValue.type != TypedValue.TYPE_STRING) {
            return true;
        }
        outValue.string = mStringBlocks[block].get(outValue.data);
        return true;
    }
    //没找到的话,返回查找失败
    return false;
}
           

        这里

loadResourceValue

方法的返回值也就是

block

其实就是我们得到的资源的值是在哪个资源包中,它表示这个所在的资源包在

AssetManager

已经加载了的所有资源包中的索引,也就是cookie - 1。为什么要有这个返回值呢?我们只关心资源找到没,至于它在哪个包中,我们这里一个int值,并不关心的,但是对于复杂的情况,它就有用了,这个我们稍后再说。

loadResourceValue

是个native方法,我们直接看其对应的jni方法:

//frameworks/base/core/jni/android_util_AssetManager.cpp
/**
 * ident id
 * density 0
 * outValue 输出参数,查寻结果存储在这里
 * resolve true,结果如果是其它资源的引用,则解析引用,直到得到最终结果
 */
static jint android_content_AssetManager_loadResourceValue(JNIEnv* env, jobject clazz,
        jint ident, jshort density, jobject outValue, jboolean resolve)
{
    //outValue存储结果,不能为空
    if (outValue == NULL) {
         jniThrowNullPointerException(env, "outValue");
         return 0;
    }
    //拿到native层AssetManager对象
    AssetManager* am = assetManagerForJavaObject(env, clazz);
    if (am == NULL) {
        return 0;
    }
    //拿到ResTable对象
    const ResTable& res(am->getResources());
    
    //存放查询结果
    Res_value value;
    //存放当前配置信息
    ResTable_config config;
    //存放typeSpecFlags
    uint32_t typeSpecFlags;
    //资源信息都在ResTable对象里,去查寻
    ssize_t block = res.getResource(ident, &value, false, density, &typeSpecFlags, &config);
    uint32_t ref = ident;
    if (resolve) {
        /**
         * 因为resolve为true,所以这里需要解析引用
         * 由于资源的引用是有可能跨包的,所以block的值需要更新
         */
        block = res.resolveReference(&value, block, &ref, &typeSpecFlags, &config);
#if THROW_ON_BAD_ID
        if (block == BAD_INDEX) {
            jniThrowException(env, "java/lang/IllegalStateException", "Bad resource!");
            return 0;
        }
#endif
    }
    if (block >= 0) {
        //返回值还是block,这个方法只是把value的值copy到outValue,并不会改变block的值
        return copyValue(env, outValue, &res, value, ref, block, typeSpecFlags, &config);
    }
    //如果没找到,如实返回,上层会进行错误处理
    return static_cast<jint>(block);
}
           

        这个方法主要做了两件事:从

ResTable

里查找资源;对查找到的资源进行引用的解析。这两个我们分开说,先说资源的查找:

//frameworks/base/libs/androidfw/ResourceTypes.cpp
/**
 * resID 要查询的资源的ID
 * outValue 用来存储查寻结果
 * mayBeBag false
 * density 0
 * outSpecFlags 输出参数
 * outConfig输出参数
 */
ssize_t ResTable::getResource(uint32_t resID, Res_value* outValue, bool mayBeBag, uint16_t density,
        uint32_t* outSpecFlags, ResTable_config* outConfig) const
{
    if (mError != NO_ERROR) {
        return mError;
    }
    //根据获取资源ID其实也就是PackageGroup的ID,拿到这个资源所在的PackageGroup在ResTable中的索引值
    //其实就是简单的移位和加减操作
    const ssize_t p = getResourcePackageIndex(resID);
    //同理,拿到资源的type索引值,其实就是简单的移位和加减操作
    const int t = Res_GETTYPE(resID);
    //同理,拿到资源的entry索引值,其实就是简单的移位和加减操作
    const int e = Res_GETENTRY(resID);
    //一系列的合法性检查
    if (p < 0) {
        if (Res_GETPACKAGE(resID)+1 == 0) {
            ALOGW("No package identifier when getting value for resource number 0x%08x", resID);
        } else {
            ALOGW("No known package when getting value for resource number 0x%08x", resID);
        }
        return BAD_INDEX;
    }
    if (t < 0) {
        ALOGW("No type identifier when getting value for resource number 0x%08x", resID);
        return BAD_INDEX;
    }

    const PackageGroup* const grp = mPackageGroups[p];
    if (grp == NULL) {
        ALOGW("Bad identifier when getting value for resource number 0x%08x", resID);
        return BAD_INDEX;
    }

    // 拿到ResTable中资源当前的配置信息
    ResTable_config desiredConfig = mParams;
    //如果指定了屏幕密度,则更新配置信息,当然我们这里density为0
    if (density > 0) {
        desiredConfig.density = density;
    }

    Entry entry;
    /**
     * 根据packageGroup、type、entry的索引值,拿到对应的ResTable::Entry对象
     * 这里是基于索引的访问,速度快
     */
    status_t err = getEntry(grp, t, e, &desiredConfig, &entry);
    if (err != NO_ERROR) {
        // Only log the failure when we're not running on the host as
        // part of a tool. The caller will do its own logging.
#ifndef STATIC_ANDROIDFW_FOR_TOOLS
        ALOGW("Failure getting entry for 0x%08x (t=%d e=%d) (error %d)\n",
                resID, t, e, err);
#endif
        return err;
    }
    /**
     * 表示这个资源有Bag,也就是所我们要获取的资源可能是bag、plurals、array、string-array、
     * integer-array、attr中的某一种,对于带有Bag的资源,系统有别的获取方式,
     * 是不允许通过AssetManager的loadResourceValue方法来获取的,这也就是为什么
     * mayBeBag在这里是false的原因。
     * 在这里我们一旦发现拿到的资源是带有Bag资源的,就返回错误,用户应该使用别的方法来获取
     */
    if ((dtohs(entry.entry->flags) & ResTable_entry::FLAG_COMPLEX) != 0) {
        if (!mayBeBag) {
            ALOGW("Requesting resource 0x%08x failed because it is complex\n", resID);
        }
        return BAD_VALUE;
    }
    /**
     * 还记得我们讲过的resources.arsc中的数据结构吗,资源的组织形式是
     * 一个ResTable_entry后面跟一个Res_value
     * 它们在一起组成一个键值对儿
     */
    const Res_value* value = reinterpret_cast<const Res_value*>(
            reinterpret_cast<const uint8_t*>(entry.entry) + entry.entry->size);

    //把结果放到outValue里
    outValue->size = dtohs(value->size);
    outValue->res0 = value->res0;
    outValue->dataType = value->dataType;
    outValue->data = dtohl(value->data);

    /**
     * 这一步很容易被忽略,前面说过我们拿到的资源有可能是别的资源包的引用,
     * 跟进一步,这个所谓的别的资源包还可能是资源共享库,我们知道资源共享库的
     * PackageGroup的id不论在编译时还是在运行时都是动态分配的,resources.arsc
     * 中记录的,也就是outValue->data的值,是编译时id;而p这个变量代表的是运行时id
     * 他们有可能是不一致的,这个时候就需要把outValue->data该为运行时id,否则会出错的
     */
    if (grp->dynamicRefTable.lookupResourceValue(outValue) != NO_ERROR) {
        ALOGW("Failed to resolve referenced package: 0x%08x", outValue->data);
        return BAD_VALUE;
    }
    //返回specFlags
    if (outSpecFlags != NULL) {
        *outSpecFlags = entry.specFlags;
    }
    //返回获取到的资源的配置信息
    if (outConfig != NULL) {
        *outConfig = entry.config;
    }
    //返回索引值,给前面的block变量,这个索引值也就是cookie值 - 1  
    return entry.package->header->index;
}
           

ResTable::getResource

方法是获取没有Bag的资源用的;对于有Bag的资源,比如style、各种array等,分别会有不同的方法来处理,我们等下再说。它根据资源id分别得到PackageGroup、type以及Entry的索引,然根据索引拿到资源值对应的Res_value。拿到后还不算完,考虑到资源共享库编译时和运行时的PackageGroup的ID有可能不同,还需要查动态引用表,这样才能得到正确的结果,最后把资源所在的包的索引值返回。

//frameworks/base/libs/androidfw/ResourceTypes.cpp
inline ssize_t ResTable::getResourcePackageIndex(uint32_t resID) const
{
    return ((ssize_t)mPackageMap[Res_GETPACKAGE(resID)+1])-1;
}

//frameworks/base/include/androidfw/ResourceTypes.h
#define Res_GETPACKAGE(id) ((id>>24)-1)
#define Res_GETTYPE(id) (((id>>16)&0xFF)-1)
#define Res_GETENTRY(id) (id&0xFFFF)
           

        我们看到其实Package(或者说PackageGroup,在不考虑overlay package的情况下,一个PackageGroup里也应该只有一个Package)的索引就是mPackageMap中对应元素值 - 1;type 的索引就是资源id的中间两位 -1;Entry的索引是资源id的后四位。我们知道一个资源的id是这种形式的:0xpptteeee,为什么呢。看到这里,大家都知道原因了吧。

        我们继续看

ResTable::getResource

方法当中获取

Entry

的那个

getEntry

方法,这个方法是资源获取过程中最关键的:

//frameworks/base/libs/androidfw/ResourceTypes.cpp
status_t ResTable::getEntry(
        const PackageGroup* packageGroup, int typeIndex, int entryIndex,
        const ResTable_config* config,/*设备的当前配置*/
        Entry* outEntry/*输出参数*/) const
{
    /**
     * 注意TypeList表示的是同一个PackageGroup中所有包的某一种类型(比如drawable)的资源
     * 一个TypeList内的每一个元素,表示每一个包中的某一种类型(比如drawable)的资源
     * 但是不考虑overlay package的话,typeList变量应该只有一个元素
     */
    const TypeList& typeList = packageGroup->types[typeIndex];
    if (typeList.isEmpty()) {
        ALOGV("Skipping entry type index 0x%02x because type is NULL!\n", typeIndex);
        return BAD_TYPE;
    }

    //用来记录当前匹配得最好的ResTable_type
    const ResTable_type* bestType = NULL;
    //用来记录当前匹配得最好的entry, 是ResTable_entry没有错,下面大家会看到
    uint32_t bestOffset = ResTable_type::NO_ENTRY;
    //用来记录当前匹配得最好的package,在考虑overlay package的时候,它才有意义,这里可以忽略
    const Package* bestPackage = NULL;
    uint32_t specFlags = 0;
    //这个还是考虑到overlay package 和 target package中同一类型的资源但是索引值不同的情况
    //这个时候就需要做转化,我们这里可以先不用考虑
    uint8_t actualTypeIndex = typeIndex;
    //用来记录当前匹配得最好的配置
    ResTable_config bestConfig;
    //每个字节都初始化为0
    memset(&bestConfig, 0, sizeof(bestConfig));

    /**
     * 一个TypeList内的每一个元素,表示每一个包中的某一种类型(比如drawable)的资源
     * 所以这里就是遍历每个包了,同样不考虑overlay package的话,typeCount = 1
     */
    const size_t typeCount = typeList.size();
    for (size_t i = 0; i < typeCount; i++) {
        //拿到ResTable::Type对象,它里面包括了,这个包内某种类型的所有配置下的资源
        //typeSpec这个变量名差评,很容易误解为resources.arsc的ResTable_typeSpec
        const Type* const typeSpec = typeList[i];

        //为了不破坏函数传过来的参数,另外定义两个变量
        int realEntryIndex = entryIndex;
        int realTypeIndex = typeIndex;
        /*overlay package 相关,可以不考虑*/
        bool currentTypeIsOverlay = false;

        //overlay package 的相关处理,这里省略了...


       // Aggregate all the flags for each package that defines this entry.
        if (typeSpec->typeSpecFlags != NULL) {
            specFlags |= dtohl(typeSpec->typeSpecFlags[realEntryIndex]);
        } else {
            specFlags = -1;
        }

        const size_t numConfigs = typeSpec->configs.size();
        //拿到所有配置下的ResTable_type,并一个一个地处理
        for (size_t c = 0; c < numConfigs; c++) {
            //拿到某种配置下的ResTable_type,config信息在这个数据结构内部
            const ResTable_type* const thisType = typeSpec->configs[c];
            if (thisType == NULL) {
                continue;
            }

            ResTable_config thisConfig;
            //拿到其配置信息
            thisConfig.copyFromDtoH(thisType->config);

            /**
             * 不匹配的话就跳过了,比如我的设备是port(config)
             * 但正在遍历的这个是land(thisConfig)
             * config为空,那就是当前设备匹配所有配置
             */
            if (config != NULL && !thisConfig.match(*config)) {
                continue;
            }

            // ResTable_type这个chunk的起始地址+size当然就是结束地址了
            const uint8_t* const end = reinterpret_cast<const uint8_t*>(thisType)
                    + dtohl(thisType->header.size);
            /**
             * ResTable_type这个chunk的起始地址+headerSize就是header后面的数据的地址了
             * 还记得我们讲resources.arsc的时候说,ResTable_type后面会跟entryCount个元素的
             * 数组吗?这个数组用来记录这个ResTable_type中每一个entry相对于entriesStart
             * 的偏移量
             */
            const uint32_t* const eindex = reinterpret_cast<const uint32_t*>(
                    reinterpret_cast<const uint8_t*>(thisType) + dtohs(thisType->header.headerSize));
            //thisOffset = 目标entry相对于entriesStart的偏移量
            uint32_t thisOffset = dtohl(eindex[realEntryIndex]);
            if (thisOffset == ResTable_type::NO_ENTRY) {
                // There is no entry for this index and configuration.
                continue;
            }

            //之前已经找到匹配的了,但是当前的这个也匹配,那就比比谁更匹配啦
            if (bestType != 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)) {
                    if (!currentTypeIsOverlay || thisConfig.compare(bestConfig) != 0) {
                        continue;
                    }
                }
            }
            //如果当前的这个更匹配,那么更新最匹配的相关记录为当前的
            bestType = thisType;
            bestOffset = thisOffset;
            bestConfig = thisConfig;
            bestPackage = typeSpec->package;
            actualTypeIndex = realTypeIndex;

            // If no config was specified, any type will do, so skip
            if (config == NULL) {
                break;
            }
        }
    }

    if (bestType == NULL) {
        return BAD_INDEX;
    }
    //再加上entriesStart,那就是当前entry相对于bestType起始地址的偏移量了
    bestOffset += dtohl(bestType->entriesStart);

    if (bestOffset > (dtohl(bestType->header.size)-sizeof(ResTable_entry))) {
        ALOGW("ResTable_entry at 0x%x is beyond type chunk data 0x%x",
                bestOffset, dtohl(bestType->header.size));
        return BAD_TYPE;
    }
    if ((bestOffset & 0x3) != 0) {
        ALOGW("ResTable_entry at 0x%x is not on an integer boundary", bestOffset);
        return BAD_TYPE;
    }
   //当前entry相对于bestType起始地址的偏移量 + bestType起始地址,就是当前entry的地址了
    const ResTable_entry* const entry = reinterpret_cast<const ResTable_entry*>(
            reinterpret_cast<const uint8_t*>(bestType) + bestOffset);
    if (dtohs(entry->size) < sizeof(*entry)) {
        ALOGW("ResTable_entry size 0x%x is too small", dtohs(entry->size));
        return BAD_TYPE;
    }
    //把结果copy给输出变量
    if (outEntry != NULL) {
        outEntry->entry = entry;
        outEntry->config = bestConfig;
        outEntry->type = bestType;
        outEntry->specFlags = specFlags;
        outEntry->package = bestPackage;
        outEntry->typeStr = StringPoolRef(&bestPackage->typeStrings, actualTypeIndex - bestPackage->typeIdOffset);
        outEntry->keyStr = StringPoolRef(&bestPackage->keyStrings, dtohl(entry->key.index));
    }
    return NO_ERROR;
}
           

getEntry

方法根据

typeIndex

packageGroup

中得到对应的

ResTable::Type

对象,并遍历它的各种配置,选择能够匹配的和之前已经找到的最合适的比较,重新确定最合适的。到最后,再根据最合适的bestType,最合适的bestOffset最终得到最合适的

ResTable_entry

。当然,我们得到

ResTable_entry

之后,它的值就在后面紧跟的那个

res_value

中。但是,

res_value

中的值也未必就是我们想要的最终值,它有可能是别的资源的引用,也就是说,

res_value->dataType

TYPE_REFERENCE

或者

TYPE_DYNAMIC_REFERENCE

,这时候

res_value->data

的值肯定是另外一个资源的引用或者说另外一个资源的ID了。

TYPE_DYNAMIC_REFERENCE

的情况比较复杂,牵涉到资源共享库,有兴趣的可以点进去详细了解,这里不再多说。对于

TYPE_REFERENCE

的 情况,需要进行解引用,否则我们得到的将会是一个id而非最终的值:

//frameworks/base/libs/androidfw/ResourceTypes.cpp

/**
 * value既是输入参数,又是输出参数
 */
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 == Res_value::TYPE_REFERENCE
            && value->data != 0 && count < 20) {/*引用的嵌套层级最多为20*/
        if (outLastRef) *outLastRef = value->data;
        uint32_t lastRef = value->data;
        uint32_t newFlags = 0;
        /**
         * 类型为Res_value::TYPE_REFERENCE,则value->data的值就是
         * 另外一个资源的id,获取之
         */
        const ssize_t newIndex = getResource(value->data, value, true, 0, &newFlags,
                outConfig);
        if (newIndex == BAD_INDEX) {
            return BAD_INDEX;
        }
        TABLE_THEME(ALOGI("Resolving reference %p: newIndex=%d, type=0x%x, data=%p\n",
             (void*)lastRef, (int)newIndex, (int)value->dataType, (void*)value->data));
        //printf("Getting reference 0x%08x: newIndex=%d\n", value->data, newIndex);
        if (inoutTypeSpecFlags != NULL) *inoutTypeSpecFlags |= newFlags;
        if (newIndex < 0) {
            //这种情况对应于,该资源是有Bag的情况,比如它是个style,
            //那么就不再继续往下解析。
            return blockIndex;
        }
        //下一级,继续解析。
        blockIndex = newIndex;
        count++;
    }
    return blockIndex;
}
           

        我们看到Android允许的资源嵌套引用层级最多为20级,其实在实际应用当中基本不会超过5级。引用层级过深,很明显会加大解析引用时循环的次数降低效率。在这个

while

循环中,Android会不断地解析

Res_value

,直到次数达到20,或者类型不再是引用为止。举个例子,假设我们的xml文件如下:

<!--framework/base/core/res/res/values/integer.xml-->
<?xml version="1.0" encoding="utf-8"?>

<resources>

    <integer name="max_lines">5</integer>

</resources>



<!--app/src/values/integer.xml-->
<?xml version="1.0" encoding="utf-8"?>

<resources>

    <integer name="text_max_lines">@android:integer/max_lines</integer>
    <integer name="max_lines">@integer/text_max_lines</integer>

</resources>
           

        在我们的app中

max_lines

的值为另外一个资源

text_max_lines

的引用,而

text_max_lines

又引用了系统资源包中的

android:integer/max_lines

android:integer/max_lines

的最终值为5。在这个例子中,存在3级引用,我们可以想象其解析流程。

        到这里,一个最简单的资源的获取流程基本就说完了,我们略去了很多Runtime Resources Overlay相关的代码没有讲,如果大家对这部分感兴趣,请点进去详细了解。