前文我们介绍了overlay package和idmap文件是怎么加载到AsseetManager中的。本文主要分析当AssetManager加载完target package和overlay package后,我们在java文件中或者xml文件里访问这些资源时,AssetManager是如何处理的。另外,本篇对Android资源管理中的Runtime Resources Overlay-------之overlay包的加载(四)的依赖非常强,毕竟资源是在加载后组织的,本篇则是去获取,所以要对资源特别是overlay package到底是如何组织在内存中的要有个总体认识。
我们就以在java文件中获取一个drawable资源为例来说明(xml中资源的获取最终和java文件中的获取大同小异,只不过是多了xml文件的解析)。我们用的比较多的就是类似mContext.getResources().getDrawable(int resId)
这样的代码,我们就从Context开始分析。前面我们讲过,Context有三个方面的意义,其中一个就是它提供了我们的应用已经加载的资源,因为它的内部有一个Resources类的实例mResources,我们看它的实现:
//frameworks/base/core/java/android/content/res/Resources.java
public Drawable getDrawable(int id, @Nullable Theme theme) throws NotFoundException {
TypedValue value;
synchronized (mAccessLock) {
value = mTmpValue;
if (value == null) {
value = new TypedValue();
} else {
mTmpValue = null;
}
getValue(id, value, true);
}
final Drawable res = loadDrawable(value, id, theme);
synchronized (mAccessLock) {
if (mTmpValue == null) {
mTmpValue = value;
}
}
return res;
}
这里的mTmpValue主要就是用来做缓存的,剩下的代码主要可以分为两部分:先去AssetManager里获取这项资源相关的信息(getValue方法),再去根据这些信息就创建具体的资源对象(loadDrawable方法)。这里的资源相关的信息是:如果这个Drawable是ColorDrawable,那么就是指具体的颜色值;如果是图片或者xml则是指图片或者xml文件的路径。我们先看getValue方法:
//frameworks/base/core/java/android/content/res/Resources.java
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));
}
这个方法去调用了AssetManager的getResourceValue()方法。需要说明的是getValue方法的这三个参数:id表示我们要找的资源的id,就是R文件里的R.drawable.icon;outValue是个输出参数,用来保存我们的查寻结果;resolveRefs表示如果我们查寻到的结果是个引用,那么我们会去解析这个引用,直到拿到具体的值为止(注意,这里有可能是多层次的引用哦)。
//frameworks/base/core/java/android/content/res/AssetManager.java
/** *ident 我们资源的id *density 0 *outValue 输出参数 *resolveRefs true 如果是引用会解析之 */
final boolean getResourceValue(int ident, int density, TypedValue outValue, boolean resolveRefs)
{
/** *block 表示我们要查询的这个资源存在于哪个包中 *注意,这里resolveRefs = true,我们得到的结果只有两种类型: *一、具体的值(这里就是颜色值了,表示一个colorDrawable) *二、字符串在对应 global string pool中的索引,表示资源(xml或者png)的路径 */
int block = loadResourceValue(ident, (short) density, outValue, resolveRefs);
if (block >= 0) {
if (outValue.type != TypedValue.TYPE_STRING) {
return true;
}
//mStringBlocks存储所有资源包的global(or value) string pool
outValue.string = mStringBlocks[block].get(outValue.data);
return true;
}
return false;
}
前面我们知道当我们的应用起来后,AssetManager至少已经加载了系统资源包framework-res.apk、我们的应用包app.apk、以及我们的overlay 包 overlay.apk。那么我们的这个变量此时应该是2,也就是我们的overlay包在AssetManager(确切地说是ResTable)中的索引。另外,我们在前文中讲过,mStringBlocks就是已经加载的资源包的global string pool(也叫 value string pool)。如果我们的drawable是个图片的话,loadResourceValue方法返回的outValue.data就是这个图片的名字在mStringBlocks[block]中的索引。loadResourceValue是个native方法,我们看其实现:
//frameworks/base/core/jni/android_util_AssetManager.cpp
static jint android_content_AssetManager_loadResourceValue(JNIEnv* env, jobject clazz,
jint ident,
jshort density,
jobject outValue,
jboolean resolve)
{
if (outValue == NULL) {
jniThrowNullPointerException(env, "outValue");
return 0;
}
//拿到AssetManager
AssetManager* am = assetManagerForJavaObject(env, clazz);
if (am == NULL) {
return 0;
}
//获取ResTable
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);
#if THROW_ON_BAD_ID
if (block == BAD_INDEX) {
jniThrowException(env, "java/lang/IllegalStateException", "Bad resource!");
return 0;
}
#endif
uint32_t ref = ident;
if (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) {
//把结果给outValue,以便java层访问
return copyValue(env, outValue, &res, value, ref, block, typeSpecFlags, &config);
}
//资源在哪个包中。 系统?app?overlay
return static_cast<jint>(block);
}
这个方法通过AssetManager最终走到了ResTable里:
ssize_t ResTable::getResource(uint32_t resID, Res_value* outValue, bool mayBeBag, uint16_t density,
uint32_t* outSpecFlags, ResTable_config* outConfig) const
{
//常规操作,拿到PackageGroup、Type、Entry的索引。
const ssize_t p = getResourcePackageIndex(resID);
const int t = Res_GETTYPE(resID);
const int e = Res_GETENTRY(resID);
/** *拿到PackageGroup,需要注意的是,此时这个packageGroup里有我们的 *app资源包,也有overlay包 */
const PackageGroup* const grp = mPackageGroups[p];
//拿到配置信息
ResTable_config desiredConfig = mParams;
if (density > 0) {
desiredConfig.density = density;
}
//拿到Entry,这是最关键的一步。
Entry entry;
status_t err = getEntry(grp, t, e, &desiredConfig, &entry);
/** *在resources.arsc中,每一个entry(如果有值,且不考虑bag资源)后面 *都会跟一个Res_value表示entry的结果 */
const Res_value* value = reinterpret_cast<const Res_value*>(
reinterpret_cast<const uint8_t*>(entry.entry) + entry.entry->size);
//把结果传给输出参数
outValue->size = dtohs(value->size);
outValue->res0 = value->res0;
outValue->dataType = value->dataType;
outValue->data = dtohl(value->data);
//输出outSpecFlags
if (outSpecFlags != NULL) {
*outSpecFlags = entry.specFlags;
}
//输出config
if (outConfig != NULL) {
*outConfig = entry.config;
}
//表示这个资源在哪个资源包中。系统?app?overlay
return entry.package->header->index;
}
需要注意的是,此时我们拿到的PackageGroup里不仅有我们的app自身,还有我们的overlay包。这个我们在Android资源管理中的Runtime Resources Overlay-------之overlay包的加载(四)中做过详细介绍,这里不再多讲。
status_t ResTable::getEntry(const PackageGroup* packageGroup, int typeIndex, int entryIndex,
const ResTable_config* config, Entry* outEntry) const
{
/** *这一句非常关键,overlay package会被放到和target package相同的那个 *PackageGroup里,TypeList也一样。overlay package中,类型为drawable的 *资源也会和target package中的drawable放到同一个TypeList中,并且 *TypeList[0]是target package的drawable *TypeList[1]以及后面的是overlay package的drawable */
const TypeList& typeList = packageGroup->types[typeIndex];
const ResTable_type* bestType = NULL;
uint32_t bestOffset = ResTable_type::NO_ENTRY;
const Package* bestPackage = NULL;
uint32_t specFlags = 0;
uint8_t actualTypeIndex = typeIndex;
ResTable_config bestConfig;
memset(&bestConfig, 0, sizeof(bestConfig));
//遍历target包中和overlay包中该类型的资源
const size_t typeCount = typeList.size();
for (size_t i = 0; i < typeCount; i++) {
//typeList[0]是target package中的资源
const Type* const typeSpec = typeList[i];
int realEntryIndex = entryIndex;
int realTypeIndex = typeIndex;
bool currentTypeIsOverlay = false;
if (typeSpec->idmapEntries.hasEntries()) {
uint16_t overlayEntryIndex;
/** * idmap中去查找,我们在上一篇文章中介绍过其实现 * 根据target entryId拿到overlay entryId */
if (typeSpec->idmapEntries.lookup(entryIndex, &overlayEntryIndex) != NO_ERROR) {
// No such mapping exists
//这里一般不会error!
continue;
}
/** *此处高能 *realEntryIndex、realTypeIndex全部改成了overlay package的!!! */
realEntryIndex = overlayEntryIndex;
realTypeIndex = typeSpec->idmapEntries.overlayTypeId() - 1;
currentTypeIsOverlay = true;
}
//......一系列的检查,略过
//开始处理每一种配置下该类的资源
const size_t numConfigs = typeSpec->configs.size();
for (size_t c = 0; c < numConfigs; c++) {
const ResTable_type* const thisType = typeSpec->configs[c];
if (thisType == NULL) {
continue;
}
ResTable_config thisConfig;
thisConfig.copyFromDtoH(thisType->config);
/** *如果当前配置匹不上设备的配置,则跳过 *比如当前这种config是德文,但设备配置的语言是中文,明显不匹配 */
if (config != NULL && !thisConfig.match(*config)) {
continue;
}
//指向thisType的结尾
const uint8_t* const end = reinterpret_cast<const uint8_t*>(thisType)
+ dtohl(thisType->header.size);
/** *在一个ResTable_type后面会有ResTable_type->entryCount个uint_32 *它们表示每一个entry相对于ResTable_type->entriesStart的偏移量 *而ResTable_type->entriesStart则表示头一个entry相对于ResTable_type的偏移量 */
const uint32_t* const eindex = reinterpret_cast<const uint32_t*>(
reinterpret_cast<const uint8_t*>(thisType) + dtohs(thisType->header.headerSize));
//该entry相对于thisType->entriesStart的偏移量
uint32_t thisOffset = dtohl(eindex[realEntryIndex]);
if (thisOffset == ResTable_type::NO_ENTRY) {//0xFFFFFFFF,不需映射时我们的填充值
// There is no entry for this index and configuration.
continue;
}
//之前已经找到了合适的配置,那么我们就要看当前的entry是否比那个更合适
if (bestType != NULL) {
//当前的没有之前的那个更合适
if (!thisConfig.isBetterThan(bestConfig, config)) {
/** *但是当前的也还匹配得上,并且当前这个是overlay package, *那还是得用当前的。 */
if (!currentTypeIsOverlay || thisConfig.compare(bestConfig) != 0) {
continue;
}
}
}
//记录匹配得最好的entry
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;
}
}
}
//匹配得最好的entry相对与bestType的偏移量
bestOffset += dtohl(bestType->entriesStart);
//拿到这个entry
const ResTable_entry* const entry = reinterpret_cast<const ResTable_entry*>(
reinterpret_cast<const uint8_t*>(bestType) + bestOffset);
//写入返回值
if (outEntry != NULL) {
outEntry->entry = entry;
outEntry->config = bestConfig;
outEntry->type = bestType;
outEntry->specFlags = specFlags;
outEntry->package = bestPackage;
//资源项的type在type string pool的索引
outEntry->typeStr = StringPoolRef(&bestPackage->typeStrings, actualTypeIndex - bestPackage->typeIdOffset);
//资源项的Name在key string pool的索引
outEntry->keyStr = StringPoolRef(&bestPackage->keyStrings, dtohl(entry->key.index));
}
return NO_ERROR;
}
从这个方法的实现我们可以看到overlay package中的资源的优先级还是比target package要高的。只要overlay package的资源能够匹配得上设备的当前配置,那么即使target package可以匹配得更好,还是会用overlay package的。但是如果overlay package的资源根本匹配不上当前设备的配置,那就起不到覆盖target package的作用了。
到这里,如果这个资源的类型是values的(color、string、integer等)、我们的查找过程就基本结束了。但是如果是xml或者图片,这里得到的只是它们的路径,此时还去加载具体的资源,这跟overlay关系已经不大,我们就不去描述了。
到这里Runtime Resources Overlay的具体使用和原理我们已经基本介绍完了。说是基本,因为还有一种静态的资源overlay,它只是在编译的时候对资源做替换,这个完全是aapt的东西了,我们在这里先不多说。
总结与说明:
AssetManager中相关的数据结构比较多,特别是ResTable_XXXX和ResTable::XXXX,我们要注意它们的区别,前者注重对resources.arsc的描述,后者是为了方便我们管理资源。
PackageGroup和TypeList的概念比较难懂,就像DynamicRefTable是为资源共享库而生一样; PackageGroup和TypeList以及IdmapEntries就是为了Runtime Resources Overlay而存在的。
阅读代码的过程中,要注意ResTable、PackageGroup、Pakage、TypeList、Type、ResTable_type、Entry这七级资源管理架构。
RRO的本质是对资源entry的映射,并将这种映射关系持久化(生成idmap文件)。如果没有持久化的过程,那就和资源的动态加载没区别了。
RRO也好,资源的动态加载框架也罢,甚至包括前面我们介绍的资源共享库,这些概念的提出,都需要我们对Android现有的资源管理框架有足够的理解,也需要有比较活跃的思维,把传统的概念迁移过来。
RRO和资源共享库是Android资源管理框架中不常用,但非常重要的概念。因为它们相关的代码在aapt、AssetManager、PMS中都有分布。如果想把Android的资源管理弄透彻,这些概念是绕不开的。