Android6.0之App中的资源查找过程

给定一个相同的资源ID,在不同的设备配置之下,查找到的可能是不同的资源。这个资源查找过程对应用程序来说,是完全透明的。现在就详细分析资源管理框架是如何根据ID来查找资源的。

资源按照是否有文件可以分为两类:。第一类资源是不对应有文件的,例如字符串资源,而第二类资源是对应有文件的,例如drawable资源。

分别对这两种情况进行分析。

资源ID格式

前面的文章中已经介绍了资源ID格式,这里在啰嗦一遍。

资源ID是一个32位四字节数字,格式:PPTTEEEE。

其中PP代表package id.系统资源的package id为0x1,而app自己的资源包package id为0x7f。0x1与0x7f之间的package id都合法。

TT代表资源的类型(type);

NNNN代表这个类型下面的资源项的名称(entry);

TT 和NNNN 的取值是由aapt工具随意指定的——基本上每一种新的资源类型的数字都是从上一个数字累加的(从1开始);而每一个新的资源entry条目也是从数字1开始向上累加的。

假设3个资源文件按照下面的顺序排列:

layout/main.xml

drawable/icon.xml

layout/listitem.xml

aapt会依次处理:

按照顺序,第一个资源的类型是”layout” 所以指定TT=1, 这个类型下面的第一个资源是”main” ,所以指定NNNN=1 ,最后这个资源就是0x7f010001。

第二个资源类型是”drawable”,所以指定TT=2,这个类型下的”icon” 指定NNNN =1,所以最终的资源ID 是 0x7f020001。

第三个资源类型是”layout”,而这个资源类型在前面已经有定义了,所以TT仍然是1,但是”listitem”这个名字是新出现的,所以指定NNNN=2,因此最终的资源ID 就是 0x7f010002。

也就是说NNNN是可以重复的,因为可以结合TT来区分。

从资源ID获取字符串资源

下面的代码用来获得一个app的名字。以此为入口点来分析字符串资源的查找过程。

  String appName = getResources().getString(R.string.app_name);

R.string.app_name是一个资源ID。

整个过程如下所示:

resources-8.jpg

其中第6步返回的Restable对象,通过上一篇的介绍已经知道了,它里面包含了本app所有的resoureces.arsc。

重点关注ResTable的getReouces方法:


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;
    }

    // 得到package  id:PP
    const ssize_t p = getResourcePackageIndex(resID);
    // 得到 type : TT
    const int t = Res_GETTYPE(resID);
    // 得到 entry: EEEE
    const int e = Res_GETENTRY(resID);

    if (p < 0) {
      ......................
        return BAD_INDEX;
    }
    if (t < 0) {
      ................
        return BAD_INDEX;
    }

    // 得到packageGroup,里面有package
    // 标准Android中app的资源包中只会包含一个package ,id为0x7f
    // 而系统资源的package  id 为 0x1
    // 也就是说PackageGroup虽然可以包含多个package,但实际上只会包含一个package
    const PackageGroup* const grp = mPackageGroups[p];
    if (grp == NULL) {
        ALOGW("Bad identifier when getting value for resource number 0x%08x", resID);
        return BAD_INDEX;
    }

    // Allow overriding density
    // 配置相关
    ResTable_config desiredConfig = mParams;
    if (density > 0) {
        desiredConfig.density = density;
    }


    Entry entry;
    // 实际上就是根据grp,t,e在resources.arsc索引资源
    status_t err = getEntry(grp, t, e, &desiredConfig, &entry);
    if (err != NO_ERROR) {
    ................
        return err;
    }
    // 根据flags判断是否是flags资源,这是处理非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;
    }
    // ResTable_entry后面跟着的就是Res_value
    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);
    if (grp->dynamicRefTable.lookupResourceValue(outValue) != NO_ERROR) {
        ALOGW("Failed to resolve referenced package: 0x%08x", outValue->data);
        return BAD_VALUE;
    }

    if (kDebugTableNoisy) {
        size_t len;
        printf("Found value: pkg=%zu, type=%d, str=%s, int=%d\n",
                entry.package->header->index,
                outValue->dataType,
                outValue->dataType == Res_value::TYPE_STRING ?
                    String8(entry.package->header->values.stringAt(outValue->data, &len)).string() :
                    "",
                outValue->data);
    }

    if (outSpecFlags != NULL) {
        // 该资源项在ResTable_typeSpec后面的spec数组中的配置
        *outSpecFlags = entry.specFlags;
    }

    if (outConfig != NULL) {
        *outConfig = entry.config;
    }
    // 返回的是资源所在的resources.arsc在ResTable。mHeaders中的索引
    return entry.package->header->index;
}

其中getEntry()用来获取Entry:

struct ResTable::Entry {
    // 该资源项的配置,来自ResTable_type.config
    ResTable_config config;
    //ResTable_entry.flags,据此可以判断资源是否是bag资源
    const ResTable_entry* entry;
    const ResTable_type* type;
    // 来自ResTable_typeSpec后面紧跟着的spec数组中对该资源项的配置
    uint32_t specFlags;
    // 所在的资源包
    const Package* package;
    // 所在资源包的类型字符串池
    StringPoolRef typeStr;
    // 所在资源包的资源项名称字符串池
    StringPoolRef keyStr;
};

前面分析resources.arsc格式的时候,已经知道了resources.arsc中的每个资源类型type,都有唯一的一个ResTable_typeSpec,其后面跟着若干ResTable_type。ResTable_type的数量为这个资源类型的配置数量。每个ResTable_type后面跟着该类型资源项的值入口,在resouces.arsc中entry是ResTable_entry。其后面跟着Res_value.当然这是非bag值时的情况。如果是bag资源的话,在resouces.arsc中entry是ResTable_map_entry,其后面跟着ResTable_map数组,数组元素数量是bag可取值的数量。

对于上面的Entry结构就很好理解了.

getEntry方法定义:

status_t ResTable::getEntry(
        const PackageGroup* packageGroup, int typeIndex, int entryIndex,
        const ResTable_config* config,
        Entry* outEntry) const

至于getEntry的代码就不贴出来了,直接说一下他的操作流程.

packageGroup中包含了资源包数据,也就包含了该资源包的所有ResTable_typeSpec。只不过这里使用下面的Type来代表一个类型:

struct ResTable::Type
{   
    // 所在资源包的索引表头部
    const Header* const             header;
    // 所在的package
    const Package* const            package;
    // 该类型资源的数量
    const size_t                    entryCount;
    // 该类型的ResTable_typeSpec,只有一个
    const ResTable_typeSpec*        typeSpec;
    // ResTable_typeSpec后面紧跟着的spec数组
    const uint32_t*                 typeSpecFlags;
    IdmapEntries                    idmapEntries;
    // 该类型所有资源项的配置
    Vector<const ResTable_type*>    configs;
};

上一篇介绍创建ResTable对象时,已经直到会利用add方法将resources.arsc加入到ResTable中,此时会调用parsePackage()方法解析resources.arsc中的package数据。其中解析到的类型规范数据块,也就是ResTable_typeSpec便以上述Type的结构保存在了packageGroup.types.

所以在genEntry()中可以通过传入的packageGroup参数,迅速拿到该package中的所有Type,即ResTable_typeSpec.

然后通过传入的第二个参数typeIndex,即类型索引,就可以直接找到该类对应的Type。

传入的第三个参数是资源在该类型中的索引,也就是在ResTable_type后面跟着的ResTable_entry的索引(实际上是ResTable_type后面紧跟着的entry偏移数组的索引)。

那么根据前面三个参数,就可以毫不费力的找到资源项对应的ResTable_entry了。只不过将找到的entry,解析数据后,封装在Entry结构中返回。

struct ResTable::Entry {
    // 该资源项的配置,来自ResTable_type.config
    ResTable_config config;
    //ResTable_entry.flags,据此可以判断资源是否是bag资源
    const ResTable_entry* entry;
    const ResTable_type* type;
    // 来自ResTable_typeSpec后面紧跟着的spec数组中对该资源项的配置
    uint32_t specFlags;
    // 所在的资源包
    const Package* package;
    // 所在资源包的类型字符串池
    StringPoolRef typeStr;
    // 所在资源包的资源项名称字符串池
    StringPoolRef keyStr;
};

而entry后面就是Res_vale(非bag情况下)了。

在回头看ResTable.getResources()方法中得到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);

java层AssetManager的jni方法loadResourceValue()中通过ResTable.getResources()得到资源项的Entry和Res_value之后,又调用ResTable.resolveReference()解析引用。

因为可能Res_value中的data,可能是一个资源ID.


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) {
        if (outLastRef) *outLastRef = value->data;
        uint32_t newFlags = 0;
        // 再次解析
        const ssize_t newIndex = getResource(value->data, value, true, 0, &newFlags,
                outConfig);
        if (newIndex == BAD_INDEX) {
            return BAD_INDEX;
        }
        if (kDebugTableTheme) {
            ALOGI("Resolving reference 0x%x: newIndex=%d, type=0x%x, data=0x%x\n",
                    value->data, (int)newIndex, (int)value->dataType, value->data);
        }
        //printf("Getting reference 0x%08x: newIndex=%d\n", value->data, newIndex);
        if (inoutTypeSpecFlags != NULL) *inoutTypeSpecFlags |= newFlags;
        if (newIndex < 0) {
            // This can fail if the resource being referenced is a style...
            // in this case, just return the reference, and expect the
            // caller to deal with.
            return blockIndex;
        }
        blockIndex = newIndex;
        count++;
    }
    return blockIndex;
}

juava层AssetManager的jni方法loadResourceValue()中调用ResTable.resolveReference()之后,又会调用copyValue()方法将数据封装为java层的TypedValue返回。

public class TypedValue {

    // 资源类型
   public int type;

   // 如果type是string,那么对应的字符串就存在这里
   public CharSequence string;

   // bag类型的资源,值就是data
   // 非bag类型的资源,其值就是字符串资源池中的索引
   public int data;

   // string 所在的resources.arsc的资源包,即apk路径,在AssetManager.mAssetPaths的索引
   // 其值减1是索引
   public int assetCookie;

   // 资源ID
   public int resourceId;


   public int changingConfigurations = -1;

   // 像素尺寸
   public int density;

}

copyValue()方法就是给TypedValue赋值的,其返回值block是resources.arsc在ResTable对象中的索引。

jint copyValue(JNIEnv* env, jobject outValue, const ResTable* table,
               const Res_value& value, uint32_t ref, ssize_t block,
               uint32_t typeSpecFlags, ResTable_config* config)
{
    // 设置TypedValue.type
    env->SetIntField(outValue, gTypedValueOffsets.mType, value.dataType);
    // 设置TypedValue.assetCookie
    env->SetIntField(outValue, gTypedValueOffsets.mAssetCookie,
                     static_cast<jint>(table->getTableCookie(block)));
    // 设置TypedValue.data
    env->SetIntField(outValue, gTypedValueOffsets.mData, value.data);
    // 设置TypedValue.string为null,后面就知道为啥了
    env->SetObjectField(outValue, gTypedValueOffsets.mString, NULL);
    // 设置TypedValue.resourceId
    env->SetIntField(outValue, gTypedValueOffsets.mResourceId, ref);
    // 设置TypedValue.changingConfigurations
    env->SetIntField(outValue, gTypedValueOffsets.mChangingConfigurations,
            typeSpecFlags);
    // 设置TypedValue.density
    if (config != NULL) {
        env->SetIntField(outValue, gTypedValueOffsets.mDensity, config->density);
    }
    return block;
}

回到java层AssetManager.getResourceText()方法:

final CharSequence getResourceText(int ident) {
        synchronized (this) {
            TypedValue tmpValue = mValue;
            int block = loadResourceValue(ident, (short) 0, tmpValue, true);
            //针对字符串,因为TypedValue中其值被设置为NULL,所以单独处理
            if (block >= 0) {
                if (tmpValue.type == TypedValue.TYPE_STRING) {
                    // TypedValue.data是资源项值在字符串值池中的索引,所以就可以直接取出来了
                    return mStringBlocks[block].get(tmpValue.data);
                }
                return tmpValue.coerceToString();
            }
        }
        return null;
    }

以上就是字符串的索引过程,可以发现这类没有资源文件对应的资源,处理是很简单的,因为这类资源的值都已经在字符串池中了,只要拿到其索引就好了。

而对于有资源文件的资源来说,还要打开资源文件进行处理。
这里还要明白一点,就是TypedValue中的type和Res_value中的dataType是一样的,但是和resources.arsc中所说的资源类型不是一回事。resources.arsc中的资源类型,以及资源ID中的资源类都是指那些attr,xml,string,drawable等类型。

而TypedValue中的type和Res_value中的dataType,是将传统的type又做了一次划分:

enum {
        // The 'data' is either 0 or 1, specifying this resource is either
        // undefined or empty, respectively.
        TYPE_NULL = 0x00,
        // The 'data' holds a ResTable_ref, a reference to another resource
        // table entry.
        TYPE_REFERENCE = 0x01,
        // The 'data' holds an attribute resource identifier.
        TYPE_ATTRIBUTE = 0x02,
        // The 'data' holds an index into the containing resource table's
        // global value string pool.
        TYPE_STRING = 0x03,
        // The 'data' holds a single-precision floating point number.
        TYPE_FLOAT = 0x04,
        // The 'data' holds a complex number encoding a dimension value,
        // such as "100in".
        TYPE_DIMENSION = 0x05,
        // The 'data' holds a complex number encoding a fraction of a
        // container.
        TYPE_FRACTION = 0x06,
        // The 'data' holds a dynamic ResTable_ref, which needs to be
        // resolved before it can be used like a TYPE_REFERENCE.
        TYPE_DYNAMIC_REFERENCE = 0x07,

        // Beginning of integer flavors...
        TYPE_FIRST_INT = 0x10,

        // The 'data' is a raw integer value of the form n..n.
        TYPE_INT_DEC = 0x10,
        // The 'data' is a raw integer value of the form 0xn..n.
        TYPE_INT_HEX = 0x11,
        // The 'data' is either 0 or 1, for input "false" or "true" respectively.
        TYPE_INT_BOOLEAN = 0x12,

        // Beginning of color integer flavors...
        TYPE_FIRST_COLOR_INT = 0x1c,

        // The 'data' is a raw integer value of the form #aarrggbb.
        TYPE_INT_COLOR_ARGB8 = 0x1c,
        // The 'data' is a raw integer value of the form #rrggbb.
        TYPE_INT_COLOR_RGB8 = 0x1d,
        // The 'data' is a raw integer value of the form #argb.
        TYPE_INT_COLOR_ARGB4 = 0x1e,
        // The 'data' is a raw integer value of the form #rgb.
        TYPE_INT_COLOR_RGB4 = 0x1f,

        // ...end of integer flavors.
        TYPE_LAST_COLOR_INT = 0x1f,

        // ...end of integer flavors.
        TYPE_LAST_INT = 0x1f
    };

从资源ID获取drawable资源的过程

drawable资源是由实际资源文件的。这类资源索引的过程大体上分为两个步骤:

  1. 解析资源ID代表的资源的路径

  2. 装载资源文件并缓存

app中获取获取drawable例子如下:

 Drawable drawable = getResources().getDrawable(R.drawable.background, getTheme());

其中R.drawable.background是drawable文件的资源ID。

整个过程如下所示:

其中第一大步骤,也就是解析资源ID代表的资源的路径,和前面字符串的索引过程是一样的,只不过前面资源项是String类型的,所以资源项的值就是想要的数据。

而这里这里资源项是drawable类型的,其值是一个文件的路径。上图中第五步返回的TypedValue中已经有了表示资源文件路径路径的字符串在资源值池中的索引等信息。

然后就是第二大步骤了,加载资源文件。这里先说明以下,针对有文件的资源,都会在第一次使用该资源时,将资源文件缓存到Resources中,下次在使用的时候,先尝试从缓存中找,有的话,就直接返回了。

以drawable为例来说,是缓存到Resources.mDrawableCache中。从图中也可以看出,加载drawable的时候,要先检查下这个缓存中是否有,有的话,直接返回,就不需要加载了。

没有缓存的话,说明还没在加载该资源文件,所以要先加载加载之后在缓存到mDrawableCache中。

而loadDrawable()方法中又是通过loadDrawableForCookie()来加载drawable的:


    private Drawable loadDrawableForCookie(TypedValue value, int id, Theme theme) {
        // drawable资源项的值是一个字符串,代表文件的路径
        if (value.string == null) {
            throw new NotFoundException("Resource \"" + getResourceName(id) + "\" ("
                    + Integer.toHexString(id) + ") is not a Drawable (color or path): " + value);
        }

        final String file = value.string.toString();

       .
        final Drawable dr;

        Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file);
        try {
            if (file.endsWith(".xml")) {
                final XmlResourceParser rp = loadXmlResourceParser(
                        file, id, value.assetCookie, "drawable");
                dr = Drawable.createFromXml(this, rp, theme);
                rp.close();
            } else {
                // 如果drawable是图片文件的话,打开它
                // assetCookie-1就是图片所在的资源包路径在native层AssetManager.mAssetPaths数组中的索引
                // 下面这个方法就是打开这个文件了
                final InputStream is = mAssets.openNonAsset(
                        value.assetCookie, file, AssetManager.ACCESS_STREAMING);
                dr = Drawable.createFromResourceStream(this, value, is, file, null);
                is.close();
            }
        } catch (Exception e) {
            Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
            final NotFoundException rnf = new NotFoundException(
                    "File " + file + " from drawable resource ID #0x" + Integer.toHexString(id));
            rnf.initCause(e);
            throw rnf;
        }
        Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);

        return dr;
    }

这个打开的过程要依赖TypedValue.assetCookie这个参数。原因很简单,因为resources.arsc中的资源文件路径:

res/drawable/........

也就是说一个相对资源包的相对路径。要打开这个文件的话,当然先要得到资源包的路径了。资源包的路径就在native层的AssetManager.mAssetPaths数组中。assetCookie-1就是要找的资源包的路径索引。

加载有资源的文件是一个耗时耗力的操作,所以都会将这些加载过的资源缓存起来:

传入的参数caches是Resources.mDrawableCache,dr是前面加载的Drawable。这样就缓存到Resources中了。

private void cacheDrawable(TypedValue value, boolean isColorDrawable, DrawableCache caches,
        Theme theme, boolean usesTheme, long key, Drawable dr) {
    final ConstantState cs = dr.getConstantState();
    if (cs == null) {
        return;
    }

    // zygote启动时,mPreloading为真
    if (mPreloading) {
      .....
    } else {
        synchronized (mAccessLock) {
            caches.put(key, theme, cs, usesTheme);
        }
    }
}

到这里为止就彻底搞清楚资源的查找与加载过程了:索引+加载+缓存。

推荐阅读更多精彩内容