news 2026/9/21 16:38:42

Fresco 二进制 XML 测试资产指南:用 AAPT 模拟编译 drawable 与 BINARY_XML 格式检测

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Fresco 二进制 XML 测试资产指南:用 AAPT 模拟编译 drawable 与 BINARY_XML 格式检测
  • 移动开发
  • 图像处理

【免费下载链接】fresco

An Android library for managing images and the memory they use.

项目地址:https://gitcode.com/gh_mirrors/fr/fresco
点击查看免费下载

导读

本文围绕 Fresco 仓库中imagepipeline-base模块的 XML 测试资源目录(imagepipeline-base/src/test/resources/com/facebook/imageformat/xmls)展开,讲解 Fresco 为何只支持加载二进制 XML(binary XML)drawable、如何用 Android 打包工具 AAPT 手工"模拟"构建期编译步骤生成测试资产,以及这些资产如何被ImageFormatChecker识别为BINARY_XML格式并被单元测试验证。读完本文,你将掌握该目录的完整结构、convert.sh脚本的运行原理与命令行细节、新增/更新测试资产的标准操作流程,以及二进制 XML 魔数检测的底层实现。

一、背景:为什么 Android 构建期必须把 raw XML 编译成二进制 XML

在 Android 中,应用构建时,res/目录下的原始(raw)XML 文件会被 AAPT(Android Asset Packaging Tool)编译为二进制 XML(binary XML)。这是因为运行时布局填充(layout inflation)并不支持直接解析原始 XML 文本——LayoutInflater要求传入的XmlPullParser来自二进制格式的资源,这从 Android 官方对LayoutInflater#inflate的约束中即可确认。

Fresco 的渲染链路(尤其是 Drawee 视图体系)依赖 Android 系统进行布局填充,因此它只支持加载二进制 XML 文件,无法直接消费原始 XML。相应地,Fresco 的图片格式检测器ImageFormatChecker也将二进制 XML 作为一类可识别的"图片格式"(BINARY_XML)纳入检测范围。

这给测试带来了一个直接问题:单元测试(尤其是 Robolectric 环境)运行时并不经过完整的 Android 构建流程,无法自动把 raw XML 编译成 binary XML。因此,Fresco 在测试资源目录中手工模拟了 AAPT 的编译步骤:将一份原始 XML 同时保留为 raw 版本,并将其编译产物(compiled 版本)一并存放在测试资源中,供测试直接读取和断言。这就是本目录存在的根本原因,详见目录内的 README.md。

二、测试资源目录结构:raw 与 compiled 一一对应

该目录位于imagepipeline-base/src/test/resources/com/facebook/imageformat/xmls/下,包含四类典型 drawable 的原始与编译版本:

文件类型说明
vector_drawable.xmlVectorDrawablepathDataviewportWidth/Heighttint的矢量图形
layer_list.xmlLayerDrawable多个<item>叠加,指定gravitytopleft偏移
state_list.xmlStateListDrawable基于state_checked/state_pressed状态切换的<selector>
level_list.xmlLevelListDrawable基于minLevel/maxLevel分级切换的<level-list>

目录布局如下:

  • raw/drawable/:未经编译的原始 XML(raw/drawable/vector_drawable.xml 等 4 个文件),是"输入";
  • compiled/:编译后的二进制 XML(compiled/vector_drawable.xml 等 4 个文件),是"输出",也是测试真正读取的对象;
  • AndroidManifest.xml:用于 AAPT 打包的最小 manifest(包名com.facebook.imageformat.xmls),见 AndroidManifest.xml;
  • convert.sh:一键完成"编译 + 提取"的 POSIX shell 脚本;
  • README.md:目录用途与操作说明。

注意state_list.xmllevel_list.xml之间存在资源引用链:state_list.xml的两个<item>分别引用@drawable/layer_list@drawable/level_list,而layer_list.xmllevel_list.xml<item>又引用@drawable/vector_drawable。这意味着 AAPT 编译时必须解析跨文件资源引用,这也正是用真实打包工具编译、而非手工伪造二进制字节的必要性所在。

三、convert.sh:用 AAPT 手工模拟构建期编译

convert.sh 是 POSIX shell 脚本,完整复刻了"资源编译 → 打包 APK → 提取编译产物"的流程:

  1. 前置检查:若未设置ANDROID_HOME环境变量,脚本直接报错退出(Environment variable ANDROID_HOME is not set)。脚本通过readlink -f定位自身所在目录,并cd到该目录,保证无论从哪里调用都能正确处理相对路径。
  2. 定位最新构建工具:扫描$ANDROID_HOME/build-tools下的所有版本目录,用sort -rn --key=4.1按版本号降序取第一个作为LATEST_TOOLS_DIR;若不存在则报错退出。同理,在$ANDROID_HOME/platforms下定位最新的android.jar平台目录。
  3. 临时目录与 APK:用mktemp -d创建临时目录,产物 APK 路径为$TMP_DIR/app.apk
  4. AAPT 打包:执行核心命令
    "$LATEST_TOOLS_DIR/aapt" package -f -m -M AndroidManifest.xml -S raw -0 "" -I "$LATEST_PLATFORM_DIR/android.jar" -F "$APK_OUTPUT"

    其中参数含义为:-f强制覆盖输出、-m生成到指定目录的 manifest、-M指定 manifest 文件、-S指定资源目录为raw-0 ""不对任何扩展名做压缩、-I引入平台android.jar作为编译时的类/资源库、-F指定输出的 APK 文件。任何一步失败都会exit 1

  5. 解压提取:进入临时目录执行unzip -q app.apk,将编译后的资源解压出来。
  6. 回填 compiled 目录:回到脚本目录,删除旧的compiled目录并重建,然后把$TMP_DIR/res/drawable/下的编译产物整体复制到./compiled
  7. 清理:删除临时目录。

需要说明的是,脚本中出现的rm -rf compiledrm -rf "$TMP_DIR"属于仓库内测试资产生成流程的一部分,仅用于维护compiled目录的纯净性;本文只做机制说明,不建议读者改动该脚本。

前置依赖:安装 Android 命令行工具

运行脚本前需要先安装 Android 命令行工具(Command-Line Tools),并确保ANDROID_HOME已配置且可见(可通过echo $ANDROID_HOME验证)。随后用sdkmanager安装构建工具与平台包,例如:

sdkmanager "build-tools;34.0.0" sdkmanager "platforms;android-33"

convert.sh会自动选用当前已安装的最新版本build-tools 与 platforms,无需在脚本中硬编码版本号。

平台限制

脚本面向POSIX 设备(macOS / Linux)。如果在 Windows 上开发,需要手工完成上述步骤(安装命令行工具、手动编译资源、从生成的 APK 中解出编译产物),或者改用 Android Studio 构建一个 APK 后从中提取编译后的 XML 资源。

四、如何添加 / 更新测试资产(标准操作流程)

结合 README.md 与脚本实现,添加或更新测试资产的标准流程为:

  1. 安装 Android 命令行工具,确认ANDROID_HOME已加入 PATH(用echo $ANDROID_HOME验证);
  2. sdkmanager安装最新(或目标)版本的 build-tools 与 platforms,例如sdkmanager "build-tools;34.0.0"sdkmanager "platforms;android-33"
  3. raw/drawable/下新增或修改原始 XML 文件(保持合法资源引用关系);
  4. 在仓库根目录执行./imagepipeline-base/src/test/resources/com/facebook/imageformat/xmls/convert.sh,脚本会自动编译并刷新compiled/目录;
  5. 校验compiled/下出现了对应的二进制 XML 文件,即可被测试引用。

一个关键原则是:raw 与 compiled 必须保持一一对应、内容同步。因为测试加载的是 compiled 版本(二进制 XML),如果只改 raw 而忘记重新运行convert.sh,测试将仍在使用旧的编译产物,导致结果与预期不符。

五、源码级原理:ImageFormatChecker 如何识别 BINARY_XML

5.1 格式常量与魔数

在 DefaultImageFormats.kt 中,BINARY_XML被定义为与 JPEG、PNG、GIF、WEBP 等并列的格式常量:

val BINARY_XML: ImageFormat = ImageFormat("BINARY_XML", "xml")

在 DefaultImageFormatChecker.kt 中,二进制 XML 的识别依靠文件头 4 字节魔数:

private val BINARY_XML_HEADER: ByteArray = byteArrayOf(3, 0, 8, 0) private const val BINARY_XML_HEADER_LENGTH: Int = 4

这 4 个字节以小端序解读,对应 Android 二进制资源文件中 XML 节点(RES_XML_TYPE,类型值0x0003)与头部大小(0x0008)。源码注释明确说明:这是二进制 XML 文件的前 4 个字节,且只能支持二进制 XML 而非原始 XML,因为 Android 在填充 drawable 时明确禁止原始 XML——二进制 XML 由构建期的 AAPT 生成。检测逻辑isBinaryXmlHeader要求headerSize >= 4且头部与魔数匹配,命中后返回DefaultImageFormats.BINARY_XML

5.2 默认关闭的 binaryXmlEnabled 开关

在 ImageFormatChecker.kt 中,ImageFormatChecker维护了一个默认值为falsebinaryXmlEnabled标志:当检测结果是BINARY_XML且该开关未开启时,结果会被过滤掉(if (this == DefaultImageFormats.BINARY_XML && !binaryXmlEnabled))。这是为了防止在未显式声明支持二进制 XML 的调用路径中误报格式。

这也解释了测试中的前置步骤:在 ImageFormatCheckerTest.kt 的setUp()中,测试会先调用ImageFormatChecker.instance.setBinaryXmlEnabled(true)打开开关,随后才能对 XML 资源断言BINARY_XML

5.3 测试如何消费 compiled 资产

ImageFormatCheckerTest.kt 中定义了 4 个针对 XML 资源的测试:

  • testXmlVectorDrawable:加载xmls/compiled/vector_drawable.xml,断言BINARY_XML
  • testXmlLayerListDrawable:加载xmls/compiled/layer_list.xml,断言BINARY_XML
  • testXmlLevelListDrawable:加载xmls/compiled/level_list.xml,断言BINARY_XML
  • testXmlStateListDrawable:加载xmls/compiled/state_list.xml,断言BINARY_XML

测试通过getResourceAsStream("xmls/compiled/...")以类路径资源方式读取编译产物,经ImageFormatChecker.getImageFormat(InputStream)检测后,用 AssertJ 的isSameAs与期望格式做同一性断言。这类测试与同文件中的 JPEG/PNG/GIF/WEBP/HEIF 格式测试共用同一套singleImageTypeTest辅助方法,验证的是格式检测器对编译后 XML 魔数的识别能力,而不是 drawable 的绘制行为——这也正是该目录只存放二进制 XML 头及结构完整文件的原因。

六、工程意义与注意事项

  1. 真实打包工具 > 手工伪造字节raw里的资源存在跨文件引用(selector → layer/level list → vector drawable),只有用 AAPT 真实编译,才能保证测试资产与线上 APK 内资源的二进制结构完全一致,避免"能通过测试但实际无法 inflate"的假阳性。
  2. compiled 资产必须入库:测试运行时没有构建期编译步骤,因此编译产物需要作为静态资源提交到仓库,与 raw 版本保持同步。任何 raw 变更都应当重新运行convert.sh并提交新的 compiled 文件。
  3. 版本依赖:脚本采用"取最新安装版本"的策略,因此不同开发机上的 build-tools / platforms 版本差异可能导致编译产物字节差异。若需稳定复现,可为脚本指定固定版本(当前仓库未硬编码)。
  4. BINARY_XML 是 Fresco 格式体系的一等公民:从DefaultImageFormats的注册列表与ImageFormatChecker的开关设计可以看出,Fresco 将二进制 XML 视作与其他位图格式并列的可识别格式,是否启用由调用方通过setBinaryXmlEnabled(true)显式声明,这一设计既保证了格式检测的完备性,也保持了默认行为的安全边界。

延伸阅读

  • 格式检测器的完整实现:DefaultImageFormatChecker.kt
  • 格式枚举与注册:DefaultImageFormats.kt
  • 检测入口与开关设计:ImageFormatChecker.kt
  • 对应单元测试:ImageFormatCheckerTest.kt
  • 本目录说明文档:xmls/README.md
  • 编译脚本:xmls/convert.sh
  • 移动开发
  • 图像处理

【免费下载链接】fresco

An Android library for managing images and the memory they use.

项目地址:https://gitcode.com/gh_mirrors/fr/fresco
点击查看免费下载
上一篇:开源项目教程:developer-roadmap
下一篇:开源项目安装与配置指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/21 16:26:23

PVE中Intel核显直通LXC的真相与绕过方案

1. 为什么 Intel 核显直通 LXC 在 PVE 7.1–8 上是个“伪需求陷阱”你搜到这篇指南&#xff0c;大概率是因为——刚在 PVE Web 界面里点开 LXC 容器设置页&#xff0c;发现“设备”栏下赫然写着“GPU 设备直通”&#xff0c;旁边还配了个小图标&#xff1b;再一查 Intel UHD Gr…

作者头像 李华