Android资源查找分析

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,争取呈现地更加细致,更加清晰。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,376评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,126评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,966评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,432评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,519评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,792评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,933评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,701评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,143评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,488评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,626评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,292评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,896评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,742评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,324评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,494评论 2 348

推荐阅读更多精彩内容