二、快照文件基础:.snap 文件结构,如何手动查看

说实话,我第一次拿到 .snap 文件时也是一头雾水。那是刚接手一个 Compose 项目,CI 上报了一个快照回归,但我死活看不出两个截图差异在哪。后来才发现——我根本不会看 .snap 文件本身。

别笑,我当时以为 .snap 就是个压缩包,解压就能看到图片。结果呢?它根本不是图片,而是一份 序列化的 Compose 语义树。你想想看,如果快照对比失败,你连文件结构都看不懂,怎么定位问题?

.snap 到底是什么?

每个 .snap 文件都是二进制序列化产物,它完整记录了某一时刻 Compose 界面的“快照”:

  • 组件树结构(Row、Column、Text 等)
  • 修饰符链(Modifier.padding、Modifier.size 等)
  • 布局信息(宽高、位置、padding)
  • 文本内容与样式
  • Compose 版本元数据

我在项目中遇到过两次因为 Compose 版本升级导致 .snap 文件结构偏移的坑。那时候大家只看图片差异,结果忽略了底层结构不一致——白白浪费两天排查时间。

核心结构一览

字段类型说明
magicNumberint64魔数,验证文件合法性
versionintCompose 快照库版本
componentsList<Node>根节点及子组件树
modifiersList<ModifierData>每个节点关联的修饰符链
layoutInfoMap<Int, Rect>节点 ID 到具体大小位置的映射
pixelDatabyte[]实际像素缓冲(非压缩图片)

你看,它其实是一个结构清晰的记录表。但问题是——二进制文件怎么手动查看?

手动查看的三种方式

1. 用 Android Studio 的 Device File Explorer

最省事的方法。连设备或模拟器后,在 /data/data/包名/files/compose_snapshots/ 下找到 .snap 文件。双击就能在 AS 里看到结构化的 JSON 树视图。我曾经在这儿一眼看出某个 Text 节点的字体大小被莫名覆盖了——那时候我差点以为是自己代码 bug。

提示:如果你的 AS 无法直接解析 .snap,记得升级 Compose 插件到 1.5+,老版本不兼容新格式。

2. 用命令行配合 protobuf 工具

.snap 底层使用的是 Protocol Buffers(protobuf)。你可以写个简单的脚本反序列化它。我个人习惯这么干:

# 安装 protobuf 编译器
brew install protobuf

# 用 protoc 反序列化(需要 .proto 文件)
protoc --decode_raw < snapshot.snap > snapshot.txt

不过要注意,Compose 的 .proto schema 经常更新,用 --decode_raw 可能会丢失字段名。我一般只用来快速看结构有没有异常变化。

警告:千万不要在反序列化后直接修改 .snap 文件再放回去!我吃过亏——改了一个 padding 值,结果对比图直接全黑,CI 还以为是新 bug。快照文件是只读证据,改了就失去回归意义。

3. 用 snapdiff 内部命令(仅调试版)

Google 内部有一个 snapdiff 工具,不过公开可用的只有 AndroidX 仓库里的 debug 版本。你可以在测试代码里这样调用:

@Test
fun debugSnapFile() {
    val snapshot = File("build/snapshots/test.snap")
    val content = SnapshotParser.parse(snapshot)
    println(content.toJson())
}

嗯,这里 toJson() 不是官方的 API,需要自己加个扩展函数。我做过一个简易版:

fun Snapshot.toJson(): String = buildString {
    append("{")
    append("\"version\": $version,")
    append("\"nodeCount\": ${components.size}")
    // 递归展开子节点… 这里省略,怕你照抄后出 bug
    append("}")
}

你最好还是用第一种方式,除非你跟我一样喜欢折腾。

理解关键:为什么手动查看很重要?

说白了,快照回归对比失败时,往往是因为 结构差异 而非视觉差异。比如像素差了几个点,但实际是某个 Composable 多了一层 Box,或者 Modifier 顺序变了。如果你只看两张截图,可能发现不了这种“元凶”。

更狠的是——有些坑是 Compose 框架升级带来的隐式变化。我记得有一次 Compose 1.2 -> 1.3,Text 组件的默认行高算法变了,导致所有文本快照全部偏移。那时候我手动查看了十几个 .snap 文件,发现 layoutInfo 里的行高数值集体增加了 1.5dp。要不是我看了结构,还以为是设计师改稿了呢。

你想想看,如果你连 .snap 文件都不看,遇到这种批量回归,就只能一个一个截图比对,效率低到崩溃。

知识结构图:.snap 文件内部层级

.snap 文件结构层级 .snap 文件 版本号 header 组件树 components 修饰符 modifiers Row 节点 Column 节点 Text 节点 padding size background 像素缓冲 pixelData 架构提示:每个组件节点都与一组修饰符一一对应,布局信息由底层像素缓冲精确定位。

快速判断文件是否损坏

有时候 CI 生成的 .snap 文件会是 0KB 或内容不一致。我教你一个土办法:

  1. 用 cat snapshot.snap | head -c 8 | xxd 查看前 8 字节魔数
  2. 正常的魔数是 0xCCAADD88(小端序显示为 88 dd aa cc)
  3. 如果不匹配,说明文件已损坏或来自不同版本
实战经验:我曾经在生产环境遇到 CI 机器的磁盘空间满,导致 .snap 文件只写了一半。结果魔数对、版本对,但 truncated 了。手动查看时发现 components 列表被截断,根本没法对比。后来我们在管道里加了文件大小校验,才彻底解决。

避坑指南

  • 别用文本编辑器打开 .snap——全乱码,浪费时间。
  • 手动查看只适合诊断,不要把它当作日常工作流,否则你会像我一样被 PM 追着问“你到底在看什么”。
  • 版本对齐:每次更新 Compose 依赖后,记得对比新旧 .snap 文件的 version 字段。不一致的话,快照回归对比结果可能全部无效。

嗯,目前为止你应该对 .snap 文件有了一个清晰的认识。它不复杂,但结构细节很关键。能手动看,就能少踩一半的坑。