- 移动开发
- 图像处理
【免费下载链接】fresco
An Android library for managing images and the memory they use.
导读
本文围绕 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.xml | VectorDrawable | 带pathData、viewportWidth/Height与tint的矢量图形 |
layer_list.xml | LayerDrawable | 多个<item>叠加,指定gravity、top、left偏移 |
state_list.xml | StateListDrawable | 基于state_checked/state_pressed状态切换的<selector> |
level_list.xml | LevelListDrawable | 基于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.xml和level_list.xml之间存在资源引用链:state_list.xml的两个<item>分别引用@drawable/layer_list和@drawable/level_list,而layer_list.xml与level_list.xml的<item>又引用@drawable/vector_drawable。这意味着 AAPT 编译时必须解析跨文件资源引用,这也正是用真实打包工具编译、而非手工伪造二进制字节的必要性所在。
三、convert.sh:用 AAPT 手工模拟构建期编译
convert.sh 是 POSIX shell 脚本,完整复刻了"资源编译 → 打包 APK → 提取编译产物"的流程:
- 前置检查:若未设置
ANDROID_HOME环境变量,脚本直接报错退出(Environment variable ANDROID_HOME is not set)。脚本通过readlink -f定位自身所在目录,并cd到该目录,保证无论从哪里调用都能正确处理相对路径。 - 定位最新构建工具:扫描
$ANDROID_HOME/build-tools下的所有版本目录,用sort -rn --key=4.1按版本号降序取第一个作为LATEST_TOOLS_DIR;若不存在则报错退出。同理,在$ANDROID_HOME/platforms下定位最新的android.jar平台目录。 - 临时目录与 APK:用
mktemp -d创建临时目录,产物 APK 路径为$TMP_DIR/app.apk。 - 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。 - 解压提取:进入临时目录执行
unzip -q app.apk,将编译后的资源解压出来。 - 回填 compiled 目录:回到脚本目录,删除旧的
compiled目录并重建,然后把$TMP_DIR/res/drawable/下的编译产物整体复制到./compiled。 - 清理:删除临时目录。
需要说明的是,脚本中出现的rm -rf compiled与rm -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 与脚本实现,添加或更新测试资产的标准流程为:
- 安装 Android 命令行工具,确认
ANDROID_HOME已加入 PATH(用echo $ANDROID_HOME验证); - 用
sdkmanager安装最新(或目标)版本的 build-tools 与 platforms,例如sdkmanager "build-tools;34.0.0"、sdkmanager "platforms;android-33"; - 在
raw/drawable/下新增或修改原始 XML 文件(保持合法资源引用关系); - 在仓库根目录执行
./imagepipeline-base/src/test/resources/com/facebook/imageformat/xmls/convert.sh,脚本会自动编译并刷新compiled/目录; - 校验
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维护了一个默认值为false的binaryXmlEnabled标志:当检测结果是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 头及结构完整文件的原因。
六、工程意义与注意事项
- 真实打包工具 > 手工伪造字节:
raw里的资源存在跨文件引用(selector → layer/level list → vector drawable),只有用 AAPT 真实编译,才能保证测试资产与线上 APK 内资源的二进制结构完全一致,避免"能通过测试但实际无法 inflate"的假阳性。 - compiled 资产必须入库:测试运行时没有构建期编译步骤,因此编译产物需要作为静态资源提交到仓库,与 raw 版本保持同步。任何 raw 变更都应当重新运行
convert.sh并提交新的 compiled 文件。 - 版本依赖:脚本采用"取最新安装版本"的策略,因此不同开发机上的 build-tools / platforms 版本差异可能导致编译产物字节差异。若需稳定复现,可为脚本指定固定版本(当前仓库未硬编码)。
- 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.
相关推荐
Composio CLI 端到端测试指南:基于 Docker 与编译二进制的 E2E 测试体系
Composio CLI 端到端测试指南:基于 Docker 与编译二进制的 E2E 测试体系 本篇技术指南以 Composio 仓库中 .agents/ski
人工智能AI Agent工具调用MCP 服务MCP ClientsAFL QEMU模式:跨架构二进制模糊测试实战指南
American Fuzzy Lop AFL 作为业界领先的安全导向模糊测试工具,其QEMU模式为二进制程序的安全测试提供了革命性的解决方案。AFL QEMU模
网络安全应用安全测试开发工具MiniMax-H3-Turbo-Lora-Pruned-ComfyUI vs 其他音视频生成工具:全面对比测评
MiniMax H3 Turbo Lora Pruned ComfyUI vs 其他音视频生成工具:全面对比测评 MiniMax H3 Turbo Lora P
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考