深入剖析 resources.arsc:二进制资源索引表

如果你玩过 Android 逆向或者做过包体优化,一定对 resources.arsc 不陌生。这个文件不大,但乾坤都在里面。它本质上是 Android 资源索引的“字典” —— 把 R 文件里的整型 ID 对应到实际资源路径、配置和值。

我记得刚入行那会儿,用 AS 反编译一个 APK,看到这个文件完全不知道怎么下手。后来做性能优化,要裁剪多语言资源,才真正把它的结构啃了一遍。说白了,它就是一张精心设计的二进制查找表。

整体布局:一个 Chunk 嵌套的世界

resources.arsc 不是纯文本,而是由多个 Chunk(块)构成的二进制流。每个 Chunk 都有一个固定头部:

struct ResChunk_header {
    uint16_t type;    // chunk 类型
    uint16_t headerSize; // 本 chunk 头部大小
    uint32_t size;    // 整个 chunk 的大小(含头部)
};

我习惯把整个文件想象成俄罗斯套娃。最外层是一个 RES_TABLE_TYPE 的 chunk,里面包了全局字符串池、资源包列表、每个包内部的条目表等等。你想想看,一个 APK 可能有多个资源包(比如默认包 + 语言包),系统在加载的时候顺着一级一级找下去。

小提示:用 010 Editor 或者 aapt2 dump resources 可以直观看到这些 chunk 的嵌套关系。我自己写过一个解析脚本,调试时特别方便。

核心数据结构:三驾马车

要理解 resources.arsc,抓住三个主题就够了:

  • 全局字符串池 (Global String Pool) – 存放所有包名、资源类型名、资源项名等文本。
  • 资源包 (Package) – 每个包有自己的类型表和资源表。
  • 资源条目 (Entry) 与值 (Value) – 最终定位到具体的资源数据。

我曾经被一个 bug 卡了两天:多语言切换后部分字符串没生效。后来用 aapt2 dump --values 一看,原来是资源包里某个 配置限定符 写错了,导致系统 fallback 到了默认值。所以了解这些结构,不只是理论,实战中排查问题很管用。

包内结构:类型表 → 条目表 → 值

每个 Package chunk 内部有一个 类型表 (Type Table),类型表下面挂载着多个 类型块 (Type Chunk)。一个类型块对应一种资源类型(比如 layout、drawable、string),里面又包含 配置列表 (Config List) 和 条目列表 (Entry List)。

我画了张图帮你理清这个关系:

resources.arsc 结构层次图 RES_TABLE (根Chunk) Global String Pool Package (0x7f) Package (其他包) Type Table (多种类型) Type Chunk (e.g. layout) Type Chunk (e.g. drawable) Type Chunk (e.g. string) Config List (语言、屏幕密度等) Entry 列表 说明:每个 Type Chunk 内部有多个配置版本(Config) 每个配置下对应一个条目索引表,条目通过值映射到实际二进制数据
关键点:资源 ID 的前两位字节是 Package ID(0x7f 为默认应用包),中间两字节是 Type ID,后两字节是 Entry ID。系统通过这个 ID 在 arsc 里做二分查找。

资源值与配置限定符

资源值不一定是字符串。可以是颜色、尺寸、Bool、整数,甚至是对布局文件的引用。每个值会关联一个 ResValue 结构:

struct Res_value {
    uint16_t size;
    uint8_t  dataType; // 比如 0x03 是字符串,0x10 是颜色
    uint16_t reserved;
    uint32_t data;     // 实际数值或字符串池索引
};

我最头疼的是 配置限定符。一个 string 条目可能对应多个值,比如 app_name 在 en-rUS 和 zh-rCN 下内容不同。系统加载时根据设备的语言、屏幕密度等,在 Config List 里找到最匹配的那个值。我曾经因为一个限位符写成了 mdpi 而不是 mdpi-v4,导致某个机型上图片始终加载不对。嗯,从那以后我检查限定符都会用 aapt2 diff 仔细核对。

实战解析:用 aapt2 dump

想知道自己的 APK 里到底有哪些资源?一条命令就够:

aapt2 dump resources your-app.apk

输出会列出所有包、类型、条目以及它们对应的值和配置。我建议你把结果重定向到文件里,然后搜一下 string 看看多语言资源是否冗余。这是包体优化的第一步。

注意:新版 Gradle 可能默认开启资源混淆(shrinkResources + minifyEnabled),但 arsc 里的条目并不会自动压缩,甚至可能残留未使用的资源 ID。这时候你就得手动分析 arsc 里的 Entry 列表。

总结一下这张表的精髓

层次作用个人经验
全局字符串池存储包名、类型名、条目名等文本可以在这里检查是否有冗余字符串
Package每个包是一个隔离域,ID 不同一般只有一个 0x7f 包,AAB 可能有多个
Type / Config资源类型 + 限定符组合写多语言要用 config 分离,别写在一个 values 里
Entry / Value最终资源的值小心 dataType,解析不对会崩溃

好了,关于 resources.arsc 的核心结构就这些。你只要把 Chunk 层次搞清楚,再结合 aapt2 或其他工具多看几遍,以后看 APK 包体就跟看自家书架一样熟悉。