天天看點

Android資源管理架構-------之最簡單的資源資訊的擷取(六)

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