Android资源查找分析

作者: 某昆 | 来源:发表于2017-08-18 17:27 被阅读398次

    1、前言

    Android的资源和代码分离,方便用户更改UI,事实上主流的编程语音都是资源和代码分离。

    资源查找非常方便,但这背后的逻辑并不简单,甚至很复杂。同时开发者也会经常遇到一些与资源相关的问题,比如主题包机制,比如动态加载时资源引用等,要解决这些问题必须对资源查找过程有一定地了解。

    2、资源编译基础

    资源有很多种类型,不同类型的资源编译还有不同策略。

    assets:assets文件夹中的资源不会被分配id,会被原封不动的编入apk中

    res/law:law文件夹中的资源也会被原封不动的编入apk中,但与assets不同,它的资源会被分配id

    其它:其它资源均会被分配资源id,且xml文本资源均会被编成二进制文本。如果解压一个apk,找到res/value文件夹下的任意一个文本,打开后发现变成乱码。

    3、资源匹配基础

    资源编译后将生成R文件,R文件记录着资源的id值。资源id值是有规律的

    public static final class drawable {
        public static final int aa=0x7f020000;
        public static final int reverse_big=0x7f02001b;
    }
    public static final class layout {
        public static final int eight_layout=0x7f030000;
        public static final int five_layout=0x7f030001;
    }
    

    以上是R文件中的代码,资源id均是16进制数,前两位均是7f,代表包名,后边两位是类型,代表drawable的类型为02,代表layout的类型是03,最后四位是资源的真正编号。请注意,类型的编号是会变的,并不是02就一定代表drawable。

    资源匹配规则比较简单。资源匹配一共有18个维度,在匹配时,根据当前的配置,去掉与配置相冲突的选项(比如语言、方向),然后根据维度的优先级,选择最合适的资源:

    4、Resources初始化过程分析

    在分析Resources初始化之前,提两个问题:
    1、为什么能查找apk里的资源呢?
    2、同一个apk里会有多个context引用,它们是否使用同一个resources?

    Resources初始化过程较为简单,具体流程如上,不再仔细分析了,直接回答上述两个问题。

    查看第6步,调用addAssetPath方法,参数即是apk的路径,因此可以根据路径查找到apk里的资源。特别强调此处,是因为addAssetPath这个方法太重要了,部分的动态加载框架,也要使用这个方法,添加动态库的路径,以实现不仅使用动态库的代码,还能使用动态库的资源。

    Resources最终在ResourcesManager类中完成初始化,而ResourcesManager使用了工厂模式,ResourcesManager将所有的Resources保存在一个键值对中,key与apk路径相关。因为能实现同一个apk引用同一个Resources的目的。

    public static ResourcesManager getInstance() {
        synchronized (ResourcesManager.class) {
            if (sResourcesManager == null) {
                sResourcesManager = new ResourcesManager();
            }
            return sResourcesManager;
        }
    }
    

    最后看看AssetManager的初始化方法。

      public AssetManager() {
        synchronized (this) {
            if (DEBUG_REFS) {
                mNumRefs = 0;
                incRefsLocked(this.hashCode());
            }
            init(false);
            if (localLOGV) Log.v(TAG, "New asset manager: " + this);
            ensureSystemAssets();
        }
    }
    

    init方法是一个native方法,它将初始化一个c++对象AssetManager,并将它和java对象的AssetManager一一对应起来。后续可以方便地根据当前java对象,找到对应的c++对象。

    static void android_content_AssetManager_init(JNIEnv* env, jobject clazz, jboolean isSystem)
    {
    AssetManager* am = new AssetManager();
    am->addDefaultAssets();
    //将java对象的AssetManager和c++对象的AssetManager对应起来
    env->SetLongField(clazz, gAssetManagerOffsets.mObject, reinterpret_cast<jlong>(am));
    }
    

    查找c++对象:

    AssetManager* assetManagerForJavaObject(JNIEnv* env, jobject obj)
    {
    jlong amHandle = env->GetLongField(obj, gAssetManagerOffsets.mObject);
    AssetManager* am = reinterpret_cast<AssetManager*>(amHandle);
    if (am != NULL) {
        return am;
    }
    jniThrowException(env, "java/lang/IllegalStateException", "AssetManager has been finalized!");
    return NULL;
    }
    

    AssetManager的构造方法中,还初始化了system的AssetManager,这也是apk中能获取系统资源的原因。

    private static void ensureSystemAssets() {
        synchronized (sSync) {
            if (sSystem == null) {
                AssetManager system = new AssetManager(true);
                system.makeStringBlocks(null);
                sSystem = system;
            }
        }
    }
    

    5、资源查找

    资源查找过程比较复杂,尤其是对于本人这样的java选手,看c++代码略显吃力,c++最让人无奈的是,有时某些方法找不到源码位置。直入正题。

    第1、2步,Resources查找integer类型的资源

    第3步,AssetManager类调用getResourceValue方法,查找资源,此处开始牵涉jni了

    第4步,android_util_AssetManager.cpp文件中,调用android_content_AssetManager_loadResourceValue方法,此方法是整个过程中的关键,负责查找资源并最终将返回值复制到java对象TypedValue中。

    static jint android_content_AssetManager_loadResourceValue(JNIEnv* env, jobject clazz,
                                                           jint ident,
                                                           jshort density,
                                                           jobject outValue,
                                                           jboolean resolve)
    {
    //根据java对象,找出对应的c++对象
    AssetManager* am = assetManagerForJavaObject(env, clazz);
    //AssetManager获取ResTable对象,如果没有,则解析resources.arsc文件、添加系统资源、添加overlay资源等,生成ResTable
    const ResTable& res(am->getResources());
    //根据id查找资源,并且将结果保存在value中
    ssize_t block = res.getResource(ident, &value, false, density, &typeSpecFlags, &config);
    //将结果复制到java对象outValue中
    if (block >= 0) {
        return copyValue(env, outValue, &res, value, ref, block, typeSpecFlags, &config);
    }
    }
    

    第5步,AssetManager获取ResTable,ResTable可以理解为一张资源表,它是通过解析resources.arsc文件、添加系统资源、添加overlay资源一起得到的一个资源表结构。

    const ResTable* AssetManager::getResTable(bool required) const
    {
    ResTable* rt = mResources;
    //如果mResources已经初始化,则直接返回
    if (rt) {
        return rt;
    }
    mResources = new ResTable();
    //获取所有的资源路径,mAssetPaths保存的至少会有两条
    //一条是apk自身的路径,另一条是系统framework-res.apk的路径
    const size_t N = mAssetPaths.size();
    for (size_t i=0; i<N; i++) {
        //解析每一条路径的资源,生成ResTable对象
        bool empty = appendPathToResTable(mAssetPaths.itemAt(i));
        onlyEmptyResources = onlyEmptyResources && empty;
    }
    return mResources;
    }
    

    ResTable中有一个非常重要的对象,最终就是从此找到资源的详细信息,appendPathToResTable方法就是为了生成mPackageGroups数据。

    // Array of packages in all resource tables.
    Vector<PackageGroup*>       mPackageGroups;
    

    6789这四个步骤,其实最终目标就是为了填充mPackageGroups的数据,在第8步中,解析resources.arsc文件,生成Asset对象,Asset对象负责读取文件之类,第9步,添加生成的Asset对象,最后mPackageGroups被填充。

    bool AssetManager::appendPathToResTable(const asset_path& ap) const {
    Asset* ass = NULL;
    if (ap.type != kFileTypeDirectory) {
        if (sharedRes == NULL) {
            if (ass == NULL) {
                //解析apk中的resources.arsc文件
                ass = const_cast<AssetManager*>(this)->
                    openNonAssetInPathLocked("resources.arsc",Asset::ACCESS_BUFFER,ap);
            }
        }
    }
    if ((ass != NULL || sharedRes != NULL) && ass != kExcludedAsset) {
        //将解析成果添加到mResources中
        if (sharedRes != NULL) {
            mResources->add(sharedRes);
        } else {
            mResources->add(ass, idmap, nextEntryIdx + 1, !shared);
        }
    }
    return onlyEmptyResources;
    }
    

    ResTable的add方法将添加PackageGroup数据结构。

    status_t ResTable::addInternal(const void* data, size_t dataSize, const void* idmapData, size_t idmapDataSize,
        const int32_t cookie, bool copyData)
    {
    while (((const uint8_t*)chunk) <= (header->dataEnd-sizeof(ResChunk_header)) &&
           ((const uint8_t*)chunk) <= (header->dataEnd-dtohl(chunk->size))) {
        //获取类型
        const uint16_t ctype = dtohs(chunk->type);
        if (ctype == RES_TABLE_PACKAGE_TYPE) {
            //如果是资源包类型,解析并且添加PackageGroup
            if (parsePackage((ResTable_package*)chunk, header) != NO_ERROR) {
                return mError;
            }
            curPackage++;
        }
        chunk = (const ResChunk_header*)
            (((const uint8_t*)chunk) + csize);
    }
    return mError;
    }
    

    第10中,根据要查找资源的id相关信息,找出要查找资源详细信息。

    ssize_t ResTable::getResource(uint32_t resID, Res_value* outValue, bool mayBeBag, uint16_t density,
        uint32_t* outSpecFlags, ResTable_config* outConfig) const
    {
    //根据资源id,解析出包名、类型、资源索引,资源id的组成详见上文
    const ssize_t p = getResourcePackageIndex(resID);
    const int t = Res_GETTYPE(resID);
    const int e = Res_GETENTRY(resID);
    //根据解析结果,得到PackageGroup
    const PackageGroup* const grp = mPackageGroups[p];
    Entry entry;
    //调用getEntry,得到查询结果
    status_t err = getEntry(grp, t, e, &desiredConfig, &entry);
    }
    

    第11步中,查找PackageGroup数据,根据当前机器的配置,找出最合适的结果。

    status_t ResTable::getEntry(
        const PackageGroup* packageGroup, int typeIndex, int entryIndex,
        const ResTable_config* config,
        Entry* outEntry) const
    {
    const TypeList& typeList = packageGroup->types[typeIndex];
    //最优类型结果
    const ResTable_type* bestType = NULL;
    //最优资源偏移量结果
    uint32_t bestOffset = ResTable_type::NO_ENTRY;
    //最优资源包结果
    const Package* bestPackage = NULL;
    //最优配置结果
    ResTable_config bestConfig;
    
    //遍历包中所有的类型,看看哪个匹配
    const size_t typeCount = typeList.size();
    for (size_t i = 0; i < typeCount; i++) {
        const Type* const typeSpec = typeList[i];
    
        int realEntryIndex = entryIndex;
        int realTypeIndex = typeIndex;
        bool currentTypeIsOverlay = false;
    
        const size_t numConfigs = typeSpec->configs.size();
        for (size_t c = 0; c < numConfigs; c++) {
            const ResTable_type* const thisType = typeSpec->configs[c];
    
            ResTable_config thisConfig;
            thisConfig.copyFromDtoH(thisType->config);
            // 检查资源是否匹配,匹配规则详见前文,不匹配则continue,遍历下一条
            if (config != NULL && !thisConfig.match(*config)) {
                continue;
            }
            //找出各项最佳参数
            bestType = thisType;
            bestOffset = thisOffset;
            bestConfig = thisConfig;
            bestPackage = typeSpec->package;
            actualTypeIndex = realTypeIndex;
        }
    }
    //保存结果
    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;
    }
    

    6、结束语

    资源机制相当复杂,本文还不够详细,让我们继续Read the fucking source code。后续我会加log,争取呈现地更加细致,更加清晰。

    相关文章

      网友评论

        本文标题:Android资源查找分析

        本文链接:https://www.haomeiwen.com/subject/hptnrxtx.html