前文我们说过了Android资源索引表,也就是resources.arsc是如何一步一步加载到内存,然后保存在资源管理相关的数据结构中的。本文,我们说说,当我们去访问资源相关的信息时,Android是如何一步一步把结果从这些数据结构中提取出来,返回给我们的。其实,当我们调用Resources.java
中的相关方法去获取资源的时候,大多数情况下,系统是要分两步走的:先获取资源相关的信息,再根据这个信息去创建或者查找(有缓存的情况)或者加载资源本身。当然,对于一些比较简单的资源,系统是可以省掉第二步,直接拿到资源本身的,比如boolean、integer、float、String等。我们就先从这些简单的资源开始分析。
Android中的Integer资源就是一个整数值,非常简单,假如我们的xml中是这样的
<resources>
<integer name="max_lines">25integer>
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文件如下:
<resources>
<integer name="max_lines">5integer>
resources>
<resources>
<integer name="text_max_lines">@android:integer/max_linesinteger>
<integer name="max_lines">@integer/text_max_linesinteger>
resources>
在我们的app中max_lines
的值为另外一个资源text_max_lines
的引用,而text_max_lines
又引用了系统资源包中的android:integer/max_lines
,android:integer/max_lines
的最终值为5。在这个例子中,存在3级引用,我们可以想象其解析流程。
到这里,一个最简单的资源的获取流程基本就说完了,我们略去了很多Runtime Resources Overlay相关的代码没有讲,如果大家对这部分感兴趣,请点进去详细了解。