资源翻译官(aapt)与智能仓库(AssetManager/Resources)

76 阅读10分钟

想象你开发了一个多语言、多屏幕适配的 Android 应用。你的资源(图片、文字、布局)就像一堆不同语言、不同尺寸的“货物”。为了让 App 在全球各种手机上都能正确显示,Android 设计了一套精密的“资源管理系统”。

​第一部分:资源编译打包 - “翻译官” aapt 的工作 (编译时)​

当你在 Android Studio 点击“Build”时,幕后英雄 aapt (Android Asset Packaging Tool) 或它的升级版 aapt2 就开始工作了。它的任务是把你的“人类可读资源”翻译成“机器高效格式”并打包进 APK。

  1. ​拆解包裹清单 (解析 AndroidManifest.xml)​

    • aapt 先看你的 AndroidManifest.xml (<manifest package="com.example.myapp">)。

    • ​目的:​​ 获取你的 App 身份证号 - ​​包名 (Package Name)​​,比如 com.example.myapp。这个包名对应资源 ID 的高位字节 0x7f

    • ​代码逻辑 (简化):​

      java
      Copy
      // aapt 内部逻辑 (伪代码)
      String packageName = parsePackageNameFromManifest(manifestFile);
      ResourceTable resourceTable = new ResourceTable(packageName); // 创建资源表,包名是身份证
      
  2. ​请教字典 (添加被引用资源包)​

    • 你的布局文件里用了 android:orientation="vertical",这个 vertical 是系统定义的资源。
    • aapt 会引用系统资源包 (framework-res.apk 里的 resources.arsc) 作为字典,理解这些系统定义的名称和 ID。
    • ​目的:​​ 知道 android:orientation 对应的资源 ID 是 0x010100c4
  3. ​收集货物清单 (收集资源文件)​

    • aapt 扫描你的 res/ 目录:

      • res/drawable-hdpi/icon.pngres/drawable-mdpi/icon.png -> 同一资源 icon 的不同配置。
      • res/layout/main.xmlres/layout-land/main.xml -> 同一布局 main 的不同配置(横屏)。
      • res/values/strings.xml -> 包含文字资源。
      • res/values/attrs.xml -> 包含自定义属性。
    • ​目的:​​ 把资源按​​类型 (Type)​​ 和​​名称 (Name)​​ 分组,并记录它们的​​配置限定符 (Qualifiers)​​(如 hdpilanden)。

  4. ​简单货物贴标签 (添加非 values 资源)​

    • 图片 (drawable)、布局 (layout) 等文件路径直接被记录到资源表 (ResourceTable)。
    • ​目的:​​ 知道 icon 这个资源在 hdpi 配置下对应文件 res/drawable-hdpi/icon.png
  5. ​文字和规则编译 (编译 values 类资源)​

    • ​字符串 (strings.xml):​

      xml
      Copy
      <string name="app_name">My App</string>
      

      aapt 把 "app_name" 作为名字,"My App" 作为值记录到资源表。

    • ​自定义属性 (attrs.xml):​

      xml
      Copy
      <attr name="custom_orientation" format="enum">
          <enum name="vertical" value="0"/>
          <enum name="horizontal" value="1"/>
      </attr>
      

      aapt 把 custom_orientation 定义为一个枚举类型属性,并记录其可能值 (vertical=0horizontal=1)。这称为一个 ​​Bag资源​​。

    • ​代码逻辑 (伪代码):​

      java
      Copy
      // 处理 strings.xml
      ResourceEntry stringEntry = resourceTable.addEntry("string", "app_name");
      stringEntry.setValue(configuration, "My App"); // configuration 可能是默认配置
      
      // 处理 attrs.xml (自定义属性是复杂资源-Bag)
      ResourceEntry attrEntry = resourceTable.addEntry("attr", "custom_orientation");
      BagResource bag = new BagResource();
      bag.addMap(ResourceHelper.attr_enum, 0); // 系统定义的 enum 类型标记
      bag.addMap(R.id.vertical, 0);    // R.id.vertical 是编译时生成的资源ID
      bag.addMap(R.id.horizontal, 1); // R.id.horizontal 是编译时生成的资源ID
      attrEntry.setComplexValue(bag);
      
  6. ​分配唯一身份证 (给资源分配 ID)​

    • aapt 为每个资源项分配一个​​唯一的 32 位资源 ID (0xPPTTEEEE)​

      • ​PP (Package ID):​​ 0x7f (你的 App) 或 0x01 (系统资源)。
      • ​TT (Type ID):​​ 资源类型编号 (0x01=anim, 0x02=color, 0x03=drawable, 0x04=layout, 0x05=string 等)。
      • ​EEEE (Entry ID):​​ 该类型下资源的顺序号。
    • ​例子:​​ icon (drawable 类型) 可能是第 1 个资源 -> 0x7f030001app_name (string 类型) 可能是第 1 个字符串 -> 0x7f050001custom_orientation (attr 类型) 可能是第 1 个属性 -> 0x7f010000

  7. ​布局文件编译 (编译 XML 资源) - 重点!​

    • 以 res/layout/main.xml 为例:

      xml
      Copy
      <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
          android:orientation="vertical"
          ...>
          <Button android:id="@+id/my_button" android:text="@string/start"/>
      </LinearLayout>
      
    • aapt 做以下转换:

      • ​属性名变 ID:​​ android:orientation -> 0x010100c4 (系统资源ID)。

      • ​属性值解析/转换:​

        • "vertical" -> 验证是合法值,并记录为整数 (通常是预定义常量)。
        • "@+id/my_button" -> 创建新 ID 0x7f020001 并赋给这个按钮。
        • "@string/start" -> 引用已存在的字符串 ID 0x7f050002
      • ​文本压缩成二进制格式 (压平 XML):​

        • ​创建字符串池:​​ 收集所有出现的字符串 ("LinearLayout""Button""orientation""id""text""http://schemas.android.com/apk/res/android" 等)。
        • ​替换文本为索引:​​ 文件里的 "Button" 变成指向字符串池位置 5 的索引。
        • ​结构化数据:​​ 使用高效的二进制结构 (ResXMLTree_nodeResXMLTree_attrExtRes_value) 描述元素、属性及其值。属性名直接用资源ID (0x010100c4),属性值用 Res_value (包含数据类型 TYPE_STRING 和索引,或者 TYPE_INT_DEC 和数值)。
        • ​最终效果:​​ 文本 main.xml 变成体积小、解析飞快的二进制文件 main.xml.flat (或在 APK 中)。
  8. ​生成资源索引目录 (生成 resources.arsc) - 核心仓库蓝图!​

    • 这是资源系统的核心数据库文件 resources.arsc

    • aapt 将所有信息按包 (Package)、类型 (Type)、配置 (Configuration) 组织:

      • ​类型字符串池:​​ "drawable""layout""string""attr"...
      • ​资源名称字符串池:​​ "icon""main""app_name""custom_orientation""my_button"...
      • ​资源值字符串池 (非必须):​​ 字符串资源的值 ("My App""Start")。
      • ResTable_package:​​ 描述你的资源包 (包名 0x7f)。
      • ResTable_typeSpec:​​ 描述每种资源类型下有哪些资源项 (icon 存在),以及它们的​​配置差异性​​ (比如 icon 有 hdpimdpi 版本)。
      • ResTable_type:​​ 描述特定配置下某个类型资源的具体信息。比如 drawable 类型在 hdpi 配置下:icon -> 文件路径 res/drawable-hdpi/icon.pngstring 类型在默认配置下:app_name -> 值 "My App"
    • ​目的:​​ 让运行时系统能根据 ​​资源ID + 设备配置​​ 快速找到最匹配的资源!

  9. ​生成资源符号表 (生成 R.java)​

    • aapt 生成 R.java 文件:

      java
      Copy
      public final class R {
          public static final class drawable {
              public static final int icon = 0x7f030001; // 对应资源ID
          }
          public static final class string {
              public static final int app_name = 0x7f050001;
          }
          public static final class id {
              public static final int my_button = 0x7f020001;
          }
          ... // 其他类型
      }
      
    • ​目的:​​ 你在代码 (Activity.java) 中能用 R.drawable.iconR.string.app_namefindViewById(R.id.my_button) 来引用资源,编译器将其替换为对应的整数 ID (0x7f030001)。

  10. ​打包进 APK​

    • aapt 将所有编译好的资源文件 (*.flat 或原始 assets/ 文件)、resources.arsc、编译好的 AndroidManifest.xml 一起打包进最终的 .apk 文件。

​第二部分:资源查找 - “智能仓库管理员” AssetManager/Resources 的工作 (运行时)​

当你的 App 在手机上运行时,系统会为它创建 Resources 和 AssetManager 对象。它们就像高效的仓库管理员,根据 resources.arsc 蓝图,快速找到当前设备最合适的资源。

  1. ​仓库开张 (初始化)​

    • ActivityThread 在创建应用时,会初始化 Resources 和 AssetManager

    • ​关键代码:​

      java
      Copy
      // 在 ActivityThread.handleBindApplication() 或类似位置
      AssetManager assetManager = new AssetManager();
      assetManager.addAssetPath(apkPath); // 加载 APK 里的资源
      Resources resources = new Resources(assetManager, displayMetrics, configuration);
      ContextImpl context = new ContextImpl(...);
      context.setResources(resources); // 关联给应用上下文
      
  2. ​查询请求 (例如 findViewById)​

    • 当你在代码中调用 getResources().getDrawable(R.drawable.icon) 或 findViewById(R.id.my_button) 时:

      • R.drawable.icon / R.id.my_button 是一个编译时常量 (如 0x7f030001)。
      • 请求被传递到 Resources 对象。
  3. ​智能仓库管理员查库 (资源查找四步曲)​
    Resources 和 AssetManager 合作,根据 ​​资源ID (0x7f030001)​​ 和 ​​当前设备配置​​ (语言、屏幕密度、方向等) 查找资源:

    • ​Step 1: 找仓库 (根据 Package ID)​

      • 拆解资源ID 0x7f030001

        • 0x7f -> 包ID (你的 App)。
        • 0x03 -> 类型ID (drawable)。
        • 0x0001 -> 资源项ID (icon)。
      • 在 resources.arsc 中找到对应包 (ResTable_package) 的数据块。

    • ​Step 2: 找货架区 (根据 Type ID)​

      • 在包数据块中,根据类型ID 0x03 找到 drawable 类型的 ​​类型规范 (ResTable_typeSpec)​​ 和 ​​当前配置下最匹配的类型资源项数据块 (ResTable_type)​​。
      • ​类型规范 (ResTable_typeSpec)​​: 告诉管理员 icon 这个资源有多个配置版本 (hdpimdpi 等)。
      • ​类型资源项数据块 (ResTable_type)​​: 存储了在当前设备配置下,drawable 类型所有资源项的具体信息。
    • ​Step 3: 精准定位 (根据 Entry ID 和配置匹配)​

      • 在找到的 ResTable_type 数据块中,根据资源项ID 0x0001 (Entry ID) 找到 icon 对应的条目 (ResTable_entry)。

      • 该条目直接指向资源值:

        • 对于 drawable: 指向APK内的文件路径 res/drawable-<density>/icon.png
        • 对于 string: 直接存储字符串值 "My App"
        • 对于 id: 仅表示一个ID存在 (用于 findViewById)。
    • ​Step 4: 取货/展示​

      • Resources 根据得到的值:

        • 如果是文件路径 (res/drawable-xxhdpi/icon.png),调用 AssetManager.openNonAsset() 读取文件内容,可能还需要解码 (如 BitmapFactory.decodeStream)。
        • 如果是字符串 ("My App"),直接返回。
        • 如果是 ID,findViewById 就用这个 ID 在视图树里查找对应的 View。
  4. ​智能匹配算法 (选择最合适的配置)​
    当 icon 有多个版本 (hdpixxhdpizhen) 时,AssetManager/Resources 如何选择?

    • ​算法核心:​​ 消除法 + 优先级。

    • ​过程 (简化):​

      1. ​排除冲突:​​ 丢弃与当前设备配置冲突的目录 (如设备语言是英文 en,就排除 values-zh 目录)。

      2. ​优先级维度 (从高到低):​

        • MCC (移动国家码) -> MNC (移动网络码) -> 语言/区域 -> 屏幕方向 -> 屏幕像素密度 (dpi) -> 触摸屏类型 -> 键盘状态 ... (完整列表见 Android 文档)。
      3. ​逐级筛选:​

        • 从最高优先级维度 (MCC) 开始检查。
        • 如果当前设备有该维度的配置 (比如 MCC=310 美国),就检查资源目录中是否有匹配该维度的 (drawable-mcc310)。有,则保留这些目录;没有,则跳过该维度。
        • 进入下一优先级维度 (MNC),重复检查。
      4. ​找到唯一或最接近:​​ 不断筛选,直到找到最匹配当前设备所有配置的那个资源目录下的资源文件。如果筛选后还有多个,选择包含限定符最多的那个 (最具体)。如果还是找不到,用默认 (drawable/values/) 目录下的。

    • ​例子:​​ 设备语言=en-GB (英国英语),屏幕方向=portrait (竖屏),密度=xxhdpi

      • 候选资源目录:drawable-en-port-xxhdpi/icon.png > drawable-en-port/icon.png > drawable-en-xxhdpi/icon.png > drawable-en/icon.png > drawable-port-xxhdpi/icon.png > ... > drawable/icon.png
      • ​胜出者:​​ drawable-en-port-xxhdpi/icon.png (最匹配)。如果没有,则选 drawable-en-xxhdpi/icon.png (语言和密度匹配,方向不冲突)。再没有才用默认的 drawable/icon.png

​总结:Android 资源管理框架的精髓​

  1. ​编译时 (aapt/aapt2):​

    • ​核心目标:​​ 将人类可读资源 (XML, PNG) 编译成高效的二进制格式 (体积小, 解析快)。

    • ​关键产出:​

      • 二进制资源文件 (*.flat, 打包进 APK)。
      • ​资源索引表 resources.arsc:​​ 核心数据库,按包、类型、配置组织所有资源信息。
      • R.java:​​ 提供 Java 代码访问资源的符号常量。
    • ​资源ID (0xPPTTEEEE):​​ 是快速查找资源的钥匙。

  2. ​运行时 (AssetManager/Resources):​

    • ​核心目标:​​ 根据 ​​资源ID (钥匙)​​ 和 ​​当前设备配置​​ (语言、屏幕等),从 APK 中快速定位并加载​​最匹配​​的资源。

    • ​关键机制:​

      • 解析 resources.arsc 索引表。
      • ​资源查找四步曲 (Package->Type->Entry->Value)。​
      • ​智能资源配置匹配算法:​​ 基于限定符优先级进行筛选。
    • ​开发者接口:​​ Resources.getXXX(R.id.name) 和 findViewById(R.id.name)

​通俗比喻:​

  • aapt 是 ​​资源翻译官兼仓库管理员​​,负责把杂乱的各种语言、尺寸的货物贴上精确的条码标签 (资源ID),按照严密的分类规则 (resources.arsc 蓝图) 打包进集装箱 (APK),并生成一份货物清单 (R.java)。
  • AssetManager/Resources 是 ​​运行时的智能分拣机器人​​。当 App 需要某件货物 (资源) 时,机器人扫描清单 (R.java 给的条码 资源ID),根据当前客户的国籍语言 (设备配置),按照蓝图 (resources.arsc) 指示的优先级规则,迅速找到仓库里最符合客户要求的那个版本的货物 (最匹配资源),并准确送达 (加载显示)。

这套机制确保了你的 App 能在全球无数不同配置的 Android 设备上,高效、正确地展示合适的界面和内容!这就是 Android 强大适配能力背后的资源管理框架。