Android中资源查找过程分析

之前博客中有一篇文章详细说明了Android中的资源打包机制和相关的arsc表结构等等的文章。这篇文章相对于前面那篇会比较上层一点,是和大家在平时开发中息息相关的资源查找过程,不过其中还是会涉及到部分的C++代码,希望大家看完之后会有收获,知道Android中的资源到底是如何被查找到的~

前言

离写上一篇文章已经过去了快两个星期,这段时间都在忙论文的事情,可能是我的导师要求比较严格吧,论文改了一遍又一遍,和他说我只想要个中等或者良好,到最后还是要写浓缩论文,公司里也请了不少时间的假了,一直希望能找到两者之间的平衡,不过论文的事也快完结了,该把之前欠的东西都补回来了~

在我们平时的开发过程中,资源的查找可以说随时随地都在被使用着,这里我们以最常见的getDrawable为例,看看它到底是如何查找的。

Java层查找过程

通过查看源码,我们可以发现Activity的getDrawable方法调用的是Context的同名函数。

1
2
3
public final Drawable getDrawable(int id) {
return getResources().getDrawable(id, getTheme());
}

而Context调用了它内部的mResource的getDrawable方法,这个Resource对象是系统通过解析apk中的arsc表和资源创建出来的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Nullable
public Drawable getDrawable(@DrawableRes int id) throws NotFoundException {
final Drawable d = getDrawable(id, null);
if (d != null && d.canApplyTheme()) {
Log.w(TAG, "Drawable " + getResourceName(id) + " has unresolved theme "
+ "attributes! Consider using Resources.getDrawable(int, Theme) or "
+ "Context.getDrawable(int).", new RuntimeException());
}
return d;
}

@Nullable
public Drawable getDrawable(@DrawableRes 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;
}

这里我们一步一步来看,首先创建了一个TypedValue对象,然后调用getValue方法,通过资源id查找到对应的资源以后将其传递到前面的那个TypedValue对象中,最后调用loadDrawable方法得到对应的资源。

我们先看后面的loadDrawable方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
@Nullable
Drawable loadDrawable(TypedValue value, int id, Theme theme) throws NotFoundException {
if (TRACE_FOR_PRELOAD) {
// Log only framework resources
if ((id >>> 24) == 0x1) {
final String name = getResourceName(id);
if (name != null) {
Log.d("PreloadDrawable", name);
}
}
}

final boolean isColorDrawable;
final DrawableCache caches;
final long key;
if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT
&& value.type <= TypedValue.TYPE_LAST_COLOR_INT) {
isColorDrawable = true;
caches = mColorDrawableCache;
key = value.data;
} else {
isColorDrawable = false;
caches = mDrawableCache;
key = (((long) value.assetCookie) << 32) | value.data;
}

// First, check whether we have a cached version of this drawable
// that was inflated against the specified theme.
if (!mPreloading) {
final Drawable cachedDrawable = caches.getInstance(key, theme);
if (cachedDrawable != null) {
return cachedDrawable;
}
}

// Next, check preloaded drawables. These may contain unresolved theme
// attributes.
final ConstantState cs;
if (isColorDrawable) {
cs = sPreloadedColorDrawables.get(key);
} else {
cs = sPreloadedDrawables[mConfiguration.getLayoutDirection()].get(key);
}

Drawable dr;
if (cs != null) {
dr = cs.newDrawable(this);
} else if (isColorDrawable) {
dr = new ColorDrawable(value.data);
} else {
dr = loadDrawableForCookie(value, id, null);
}

// Determine if the drawable has unresolved theme attributes. If it
// does, we'll need to apply a theme and store it in a theme-specific
// cache.
final boolean canApplyTheme = dr != null && dr.canApplyTheme();
if (canApplyTheme && theme != null) {
dr = dr.mutate();
dr.applyTheme(theme);
dr.clearMutated();
}

// If we were able to obtain a drawable, store it in the appropriate
// cache: preload, not themed, null theme, or theme-specific.
if (dr != null) {
dr.setChangingConfigurations(value.changingConfigurations);
cacheDrawable(value, isColorDrawable, caches, theme, canApplyTheme, key, dr);
}

return dr;
}

代码比较长,但是这个逻辑其实不是我们所关心的,因为我们这篇文章的主题是[资源的查找过程],而逻辑进行到这里其实已经是找到了对应的资源,只不过在loadDrawable方法中通过传进来的TypedValue生成对应的drawable资源。

所以我们可以肯定的是,整个资源查找过程的核心就是前面提到的getValue方法,通过这个方法得到一个TypedValue对象,里面存储着我们需要的资源。

1
2
3
4
5
6
7
8
9
public void getValue(@AnyRes 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));
}

可以看到,这个方法中调用了mAssets的getResourceValue方法,而这个mAssets对象其实是一个AssetManager类型的,也就是说,Resource对象将[通过id查找资源的这一部分逻辑]委托给了AssetManager。这也和我博客中前面那篇文章里的逻辑对应了起来,在那篇文章中有提到,解析资源的时候会在生成Resource对象的时候生成一个AssetManager对象,而这里正是用这个对象去寻找资源的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*package*/ final boolean getResourceValue(int ident,
int density,
TypedValue outValue,
boolean resolveRefs)

{

int block = loadResourceValue(ident, (short) density, outValue, resolveRefs);
if (block >= 0) {
if (outValue.type != TypedValue.TYPE_STRING) {
return true;
}
outValue.string = mStringBlocks[block].get(outValue.data);
return true;
}
return false;
}

可以看到AssetManager通过loadResourceValue方法去获取将资源赋值到对应的TypedValue对象中,并且如果返回的block小于0,则表示资源没找到,方法返回false。看到这里我们明白,资源查找的核心是AssetManager中的loadResourceValue方法。

1
2
private native final int loadResourceValue(int ident, short density, TypedValue outValue,
boolean resolve)
;

这个方法是native方法,这意味着我们必须要去看C++层的代码了,不然我们是没办法完全搞懂资源查找的过程的。

C++层查找过程

C++层的AssetManager对应的路径是android/platform/frameworks/master/./core/jni/ android_util_AssetManager.cpp,大家可以去git上查看。下面让我们来看看它的loadResourceValue方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
static jint android_content_AssetManager_loadResourceValue(JNIEnv* env, jobject clazz,
jint ident,
jshort density,
jobject outValue,
jboolean resolve)
{
AssetManager* am = assetManagerForJavaObject(env, clazz); //step1
if (am == NULL) {
return 0;
}
const ResTable& res(am->getResources()); //step 2
Res_value value;
ResTable_config config;
uint32_t typeSpecFlags;
ssize_t block = res.getResource(ident, &value, false, density, &typeSpecFlags, &config); //step3
#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) {
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
}
return block >= 0 ? copyValue(env, outValue, &res, value, ref, block, typeSpecFlags, &config) : block; //step4
}

我在代码中标注了4个step:

step1. 通过assetManagerForJavaObject方法将Java层的AssetManager转换成C++层的AssetManager。

step2. 通过AssetManager生成一个ResTable对象。

step3. 通过生成的ResTable对象的getResource方法通过ident(也就是资源id)获取对应的资源。

step4. 将资源复制到Java的TypedValue对象中。

通过上面四个主要的步骤,我们可以知道,具体的逻辑就是在ResTable的getResource方法中。

ResTable的代码对应的路径是android/platform/frameworks/master/./libs/androidfw/ResouceTypes.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
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;
}
const ssize_t p = getResourcePackageIndex(resID); //step1
const int t = Res_GETTYPE(resID); //step1
const int e = Res_GETENTRY(resID); //step1
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]; //step2
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;
status_t err = getEntry(grp, t, e, &desiredConfig, &entry); //step3
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;
}
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;
}
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); //step4
// The reference may be pointing to a resource in a shared library. These
// references have build-time generated package IDs. These ids may not match
// the actual package IDs of the corresponding packages in this ResTable.
// We need to fix the package ID based on a mapping.
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) {
*outSpecFlags = entry.specFlags;
}
if (outConfig != NULL) {
*outConfig = entry.config;
}
return entry.package->header->index;
}

代码很长,照旧,我还是在代码中标注出了几个关键的步骤。

step1. 通过resId得到对应的package,type和entry,存放在对象p,t和e中。这三个是什么意思呢,我们都知道一个资源id是怎么样表示的对吧,去一个app的R文件中看看,我们会看到类似这样的东西:

1
public static final int someAppResource=0x7f01006b;

其中0x7f表示的就是package,中间的01表示的是type而最后四位006b表示的就是entry。

其中package表示的是资源的来源,一般app都是0x7f,系统是0x01。type表示资源的类型,不同类型的资源例如String,Drawable都是不同的,而entry表示的是同一种类型资源里一个具体资源的标示。

step2. 获取对应的PackageGroup对象。这个是什么意思呢?前面说过,一般package就是0x7f和0x01两种。那为什么还会有PackageGroup这一说呢?如果大家了解过插件化开发,就会知道一个Android应用内可能不止一个apk,所以对应的0x7f的package也可能会不止一个。

step3. 通过getEntry获取具体的entry对象。这个也是我们下一步要说的逻辑。

step4. 赋值到对应的outValue,也就是TypedValue中。

通过上面的说明,我们知道具体的逻辑就是得到一个PackageGroup,并且通过对应的type和entry调用getEntry方法获取资源,那么下面就让我们来看看getEntry方法吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
status_t ResTable::getEntry(
const PackageGroup* packageGroup, int typeIndex, int entryIndex,
const ResTable_config* config,
Entry* outEntry) const{
const TypeList& typeList = packageGroup->types[typeIndex]; //step1
if (typeList.isEmpty()) {
ALOGV("Skipping entry type index 0x%02x because type is NULL!\n", typeIndex);
return BAD_TYPE;
}
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));
// Iterate over the Types of each package.
const size_t typeCount = typeList.size();
for (size_t i = 0; i < typeCount; i++) { //step2
const Type* const typeSpec = typeList[i];
int realEntryIndex = entryIndex;
int realTypeIndex = typeIndex;
bool currentTypeIsOverlay = false;
// Runtime overlay packages provide a mapping of app resource
// ID to package resource ID.
if (typeSpec->idmapEntries.hasEntries()) {
uint16_t overlayEntryIndex;
if (typeSpec->idmapEntries.lookup(entryIndex, &overlayEntryIndex) != NO_ERROR)
{
// No such mapping exists
continue;
}
realEntryIndex = overlayEntryIndex;
realTypeIndex = typeSpec->idmapEntries.overlayTypeId() - 1;
currentTypeIsOverlay = true;
}
if (static_cast<size_t>(realEntryIndex) >= typeSpec->entryCount) {
ALOGW("For resource 0x%08x, entry index(%d) is beyond type entryCount(%d)",
Res_MAKEID(packageGroup->id - 1, typeIndex, entryIndex),
entryIndex, static_cast<int>(typeSpec->entryCount));
// We should normally abort here, but some legacy apps declare
// resources in the 'android' package (old bug in AAPT).
continue;
}
// 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();
for (size_t c = 0; c < numConfigs; c++) { //step3
const ResTable_type* const thisType = typeSpec->configs[c];
if (thisType == NULL) {
continue;
}
ResTable_config thisConfig;
thisConfig.copyFromDtoH(thisType->config);
// Check to make sure this one is valid for the current parameters.
if (config != NULL && !thisConfig.match(*config)) {
continue;
}
// Check if there is the desired entry in this type.
const uint32_t* const eindex = reinterpret_cast<const uint32_t*>(
reinterpret_cast<const uint8_t*>(thisType) + dtohs(thisType->header.headerSize));
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)) { //step4
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;
}
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;
}
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;
}
if (outEntry != NULL) { //step5
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;
}

好吧。。我承认这个代码还是很多,不过不要怕,这已经是最后一步了~

step1. 通过typeIndex获取typeList。这个TypeList对象就是所有相同type资源的集合,当然前提是在同一个PackageGroup中。

step2. 循环遍历TypeList。

step3. 取出所有的Config。Config的意思就是配置项,比如drawable-xhdpi和drawable-hdpi就是两个Config。

step4. 对比前后两个Config,获取最合适的资源。

step5. 赋值到outEntry中。

总结

下面让我们一起来做一个总结。

首先在Java层,如果你是通过资源Id的形式去获取资源的,那么就会调用Resource的同名函数,而Resource类中所有资源的查找都会先通过getValue去将资源赋值到一个TypeValue中,然后通过对应的方法(比如drawable就是loadDrawable)去加载资源。而在getV内部调用AssetManager的getResourceValue方法。这个方法最终调用一个native方法loadResourceValue去寻找资源。

loadResourceValue中先获取一个ResTable,再通过ResTable的getResource去获取资源。这个方法是我们寻找资源的核心。

getResource中会通过资源Id分别获取对应的Package,Type和Entry。然后通过getEntry去获取具体的资源。在getEntry中又回去遍历一个TypeList对象,通过比较不同的Config获取最合适的资源。