3. OTA升级包格式解析
拿到一个高通平台的完整 OTA 包(通常是一个几 GB 的 update.zip),如果只把它当成一个压缩包,就很难理解升级过程中“为什么能只刷一部分分区”“为什么 A/B 槽位可以无缝切换”“为什么校验失败会整包回滚”。要回答这些问题,必须把包拆开,看清里面每一层文件各自承担什么职责。本节按“外层容器 → 分区镜像 → 增量载荷 → 元数据与属性”的顺序,逐层解析 OTA 包的真实结构。
一、外层容器:update.zip 的目录骨架
OTA 包本质是一个 ZIP 容器,但它的目录结构是被 Android 升级框架严格约定的。解压后典型内容如下(以 A/B 全量包为例):
update.zip
├── payload.bin # 真正的分区数据载荷(A/B 设备核心)
├── payload_properties.txt # payload 的哈希与大小,供校验与断点续传
├── care_map.pb # 需要校验的块范围映射
├── metadata # 包级元数据(OTA 类型、时间戳、设备约束)
├── metadata.pb # protobuf 格式的元数据(部分版本)
├── META-INF/
│ └── com/android/
│ ├── metadata # 旧式元数据
│ ├── metadata.pb
│ └── otacert # 签名证书
└── system.transfer.list / system.new.dat.br # 旧式块 OTA(非 A/B)
可以看到,A/B 设备与旧式块 OTA 的包结构差异很大。A/B 包的核心是 payload.bin,而旧式包依赖 *.transfer.list 加 *.new.dat。高通平台自 Android 10 起主流方案已全面转向 A/B + 动态分区,因此本节重点放在前者。
二、payload.bin:增量载荷的容器格式
payload.bin 不是简单拼接的镜像,而是遵循 update_engine 定义的 payload 格式。它的头部是 DeltaArchiveManifest(protobuf 编码),后面紧跟各个分区的安装操作数据。manifest 中关键字段包括:
partitions[]:每个待更新分区的名字(如system、vendor、boot)及其new_partition_info(目标大小、哈希)。install_operations[]:描述如何把数据写入目标分区,操作类型有REPLACE、REPLACE_BZ、REPLACE_XZ、ZERO、SOURCE_COPY、SOURCE_BSDIFF、BROTLI_BSDIFF等。kernel_install_operations[]:针对内核分区的独立操作序列。block_size:通常为 4096 字节,所有偏移和长度都以此为对齐单位。
全量包中,每个分区基本由若干 REPLACE_XZ 操作组成,即“把这段压缩数据解压后写到目标分区的指定块区间”。增量包中则大量出现 SOURCE_BSDIFF,它表示“读取旧分区对应块,与补丁数据做 bspatch 运算,得到新块”。这正是增量包体积远小于全量包的原因。
三、super.img 与动态分区
在动态分区方案中,system、vendor、product、system_ext 等逻辑分区不再各自独立占用物理分区,而是被统一装进一个物理分区 super。这个物理分区的内容就是 super.img。它内部由 LP metadata(liblp 格式)描述逻辑分区的布局:每个逻辑分区在 super 中的起始扇区、大小、分区组(group)以及最大可扩展空间。
动态分区的价值在于:OTA 时不必为每个逻辑分区预留固定物理空间,而是由 super 统一管理。升级时 update_engine 会先根据新 manifest 计算各逻辑分区的新尺寸,必要时调整 LP metadata,再把数据写入对应偏移。因此解析 super.img 时,第一步永远是读 LP metadata,而不是直接按固定偏移找 system。
四、metadata 与 manifest:约束与描述
包根目录的 metadata 是纯文本键值对,描述“这个包能不能装到这台设备上”。常见字段包括:
ota-type=AB或BLOCK:升级类型。post-build/post-build-incremental:目标版本指纹,用于与当前系统比对。pre-device:允许安装的设备代号,不匹配则拒绝。post-timestamp:构建时间戳,用于防降级。ota-property-files:列出包内需要校验的文件及其偏移,供流式升级直接定位。
而 manifest 在 A/B 语境下通常指 payload 内部的 DeltaArchiveManifest,它描述“每个分区怎么改”;metadata 描述“这个包能不能装”。两者一个管约束、一个管操作,缺一不可。旧式包中还存在 META-INF/com/android/metadata,作用类似,但字段更少。
五、property 文件:构建属性与兼容性
升级完成后,系统会读取新分区的 build.prop、vendor/build.prop、odm/etc/build.prop 等属性文件。它们决定了 ro.build.fingerprint、ro.vendor.build.fingerprint、ro.product.device 等关键值。OTA 校验时,pre-device 与当前 ro.product.device 比对,post-build 与目标 ro.build.fingerprint 比对,任何一项不符都会中止升级。因此 property 文件虽然不直接参与数据写入,却是升级准入判断的依据。
六、一次解析实操:从包到分区布局
下面给出一个可复现的解析流程,用于确认包结构与分区尺寸。示例命令基于通用工具,参数为合理假设值。
# 1. 查看包内文件清单
unzip -l update.zip | grep -E "payload|metadata|care_map"
# 2. 提取 payload 属性(哈希与大小)
unzip -p update.zip payload_properties.txt
# 输出示例:
# FILE_HASH=AbCd...==
# FILE_SIZE=2147483648
# METADATA_HASH=EfGh...==
# METADATA_SIZE=123456
# 3. 解析 payload manifest,查看分区与操作类型
payload-dumper-go -l payload.bin
# 输出示例(节选):
# Partition system_a: size=3221225472, ops=REPLACE_XZ x 48
# Partition vendor_a: size=1073741824, ops=SOURCE_BSDIFF x 12
# Partition boot_a: size=100663296, ops=REPLACE_XZ x 2
# 4. 查看 super 内逻辑分区布局
lpdump super.img
# 输出示例(节选):
# Group qti_dynamic_partitions_a: maximum_size=6442450944
# system_a: offset=1048576, size=3221225472
# vendor_a: offset=3222274048, size=1073741824
从输出可以读出三个关键判断:第一,system_a 的操作全是 REPLACE_XZ,说明它是全量替换;第二,vendor_a 出现 SOURCE_BSDIFF,说明该分区走增量补丁;第三,lpdump 给出的 offset 与 size 必须与 manifest 中 new_partition_info 一致,否则写入会越界或覆盖相邻分区。实际工程中,若 lpdump 显示 group 的 maximum_size 小于各逻辑分区之和,升级会在 metadata 校验阶段直接失败。
七、结构对照速查
| 文件/对象 | 格式 | 核心作用 | 解析工具 |
|---|---|---|---|
| update.zip | ZIP | 外层容器,承载全部升级资源 | unzip |
| payload.bin | update_engine payload | 分区数据与操作序列 | payload-dumper-go |
| DeltaArchiveManifest | protobuf | 描述分区如何被修改 | delta_generator |
| super.img | LP metadata + 逻辑分区 | 动态分区物理载体 | lpdump |
| metadata | 键值文本 | 设备约束与升级准入 | cat / grep |
| build.prop | 键值文本 | 版本指纹与设备属性 | grep |
把这几层关系串起来:update.zip 是外壳,metadata 决定“能不能装”,payload.bin 的 manifest 决定“怎么装”,super.img 的 LP metadata 决定“装到哪里”,而 build.prop 等 property 文件决定“装完是什么版本”。理解这条链路,后续分析升级失败日志、制作增量包或裁剪分区时,才能准确定位到具体文件与字段。