前文我們說過了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相關的代碼沒有講,如果大家對這部分感興趣,請點進去詳細了解。