OTA 包虽然是zip包,但是里面的内容payload.bin才是核心,安卓原生中没有上来就对zip包做整包验签,而是对payload.bin处理过程中做的,payload.bin已经是高度压缩内容,在zip包里采用非压缩态存储,update_engine通过外部传参直接裸读zip包。payload.bin中格式是一套自描述的二进制协议。今天我们把它拆开,看看每个字节都是干嘛的。
先回答一个问题
上一篇聊了 update_engine 的整体架构,ActionProcessor 怎么编排流水线,各个 Action 怎么各司其职。
但有个关键问题我需要深入——DeltaPerformer 到底在写什么?
DownloadAction 从指定位置获取一堆数据,DeltaPerformer 一块块往 inactive slot 里塞。这些数据长什么样?它怎么知道"这块东西该写到哪个分区的哪个位置"?
答案就藏在 OTA Payload 的格式里。
四个字母:CrAU
zip包解开后,你拿一个 OTA payload 文件,打开十六进制编辑器,前四个字节:
43 72 41 55ASCII 就是 CrAU——Chrome Auto Update 的缩写。
对,Android OTA 的 payload 格式直接沿用了 Chrome 浏览器自动更新的格式。Google 内部很早就搞了这套二进制协议,后来 Android 搞 A/B 更新,直接拿来用了,省得重新造轮子。
这四个字节就是"魔数"。任何解析器拿到数据,先看前四个字节是不是 CrAU,不是就直接报错。
整体布局:五段式
一个 OTA payload 文件,从头到尾长这样:
┌─────────────────────────────────────────────────────────┐ │ Payload 文件 │ │ │ │ ┌──────────────────────────────────┐ │ │ │ Payload Header (固定长度) │ │ │ │ ├─ magic: "CrAU" (4 bytes) │ │ │ │ ├─ major_version: 2 (8 bytes) │ │ │ │ ├─ manifest_size (8 bytes) │ │ │ │ ├─ metadata_signature_size (4B) │ │ │ │ └─ ... │ │ │ └──────────────────────────────────┘ │ │ │ │ ┌──────────────────────────────────┐ │ │ │ Manifest (protobuf 序列化) │ │ │ │ ├─ DeltaArchiveManifest │ │ │ │ │ ├─ PartitionUpdate[] │ │ │ │ │ │ ├─ partition_name │ │ │ │ │ │ ├─ operations[] │ │ │ │ │ │ └─ ... │ │ │ │ │ └─ ... │ │ │ └──────────────────────────────────┘ │ │ │ │ ┌──────────────────────────────────┐ │ │ │ Metadata Signature (可选) │ │ │ │ └─ 对 Header+Manifest 的签名 │ │ │ └──────────────────────────────────┘ │ │ │ │ ┌──────────────────────────────────┐ │ │ │ Blob Data (实际数据载荷) │ │ │ │ ├─ operation 0 的数据 │ │ │ │ ├─ operation 1 的数据 │ │ │ │ ├─ ... │ │ │ │ └─ operation N 的数据 │ │ │ └──────────────────────────────────┘ │ │ │ │ ┌──────────────────────────────────┐ │ │ │ Payload Signature │ │ │ │ └─ 对整个 payload 的签名 │ │ │ └──────────────────────────────────┘ │ └─────────────────────────────────────────────────────────┘五大部分,一段扣一段。下面这张图从字节偏移的角度拆解了每一段的细节:

Header → Manifest → Metadata Signature → Blob Data → Payload Signature,五段。
它们之间有严格的顺序依赖——你得先解析 Header 才能知道 Manifest 有多大,得解析 Manifest 才能知道 Blob Data 怎么用。一层扣一层。
Header:24 字节的开门钥匙
Header 是固定长度的,很直接。源码在 payload_metadata.h:
cpp
constexpr size_t kDeltaVersionSize = 8; constexpr size_t kDeltaManifestSize = 8; constexpr size_t kDeltaMetadataSigSize = 4; constexpr size_t kDeltaPayloadHeaderSize = kDeltaVersionSize + kDeltaManifestSize + kDeltaMetadataSigSize; // = 8 + 8 + 4 = 20 bytes(不含 magic)完整的 Header 结构:
偏移量 大小 字段名 说明 ────────────────────────────────────────────────────── 0x00 4 bytes magic "CrAU" (0x43724155) 0x04 8 bytes major_version 主版本号,目前固定为 2 0x0C 8 bytes manifest_size Manifest 部分的字节数 0x14 4 bytes metadata_signature_size 元数据签名的字节数 ────────────────────────────────────────────────────── 总共: 24 bytesParsePayloadHeader() 的核心逻辑:
cpp
ErrorCode PayloadMetadata::ParsePayloadHeader( const brillo::Blob& payload, PayloadMetadataValues* values) { if (payload.size() < kDeltaMagic.size() + kDeltaPayloadHeaderSize) return ErrorCode::kDownloadInvalidMetadataMagicString; // 校验魔数 if (!std::equal(kDeltaMagic.begin(), kDeltaMagic.end(), payload.begin())) return ErrorCode::kDownloadInvalidMetadataMagicString; uint64_t major_version; memcpy(&major_version, payload.data() + kDeltaMagic.size(), sizeof(major_version)); if (major_version != kDeltaVersionMajor) return ErrorCode::kUnsupportedMajorPayloadVersion; uint64_t manifest_size; memcpy(&manifest_size, payload.data() + kDeltaMagic.size() + kDeltaVersionSize, sizeof(manifest_size)); uint32_t metadata_sig_size; memcpy(&metadata_sig_size, payload.data() + kDeltaMagic.size() + kDeltaVersionSize + kDeltaManifestSize, sizeof(metadata_sig_size)); values->manifest_size = manifest_size; values->metadata_signature_size = metadata_sig_size; return ErrorCode::kSuccess; }有个细节:major_version 和 manifest_size 是小端序存储的。x86/ARM 原生字节序,直接 memcpy 就行,不用 ntohl 那套。
Header 解析完,就知道两件事:Manifest 有多大,签名从哪开始。
Manifest:施工图纸
Manifest 是整个 Payload 最核心的部分——一段 protobuf 序列化的二进制数据,描述"要把哪些分区改成什么样"。
proto 定义在 update_metadata.proto,这个文件是 update_engine 的灵魂。
DeltaArchiveManifest
protobuf
message DeltaArchiveManifest { repeated PartitionUpdate partitions = 2; optional uint64 max_timestamp = 14; optional uint64 minor_version = 12; optional DynamicPartitionMetadata dynamic_partition_metadata = 17; optional uint64 security_patch_level = 18; }partitions 是核心中的核心——一个数组,每个元素描述一个分区的更新。
PartitionUpdate
protobuf
message PartitionUpdate { required string partition_name = 1; // system, vendor, boot... optional bool run_postinstall = 4; optional string postinstall_path = 5; optional uint64 new_partition_info = 8; optional uint64 old_partition_info = 9; repeated InstallOperation operations = 10; // 操作列表 optional EstimateCowSize estimate_cow_size = 19; // VABC 相关 }operations 是真正的干活部分——每个元素就是一个"手术操作",告诉 DeltaPerformer "把这块数据写到哪"。
InstallOperation:12 种手术刀
protobuf
message InstallOperation { required Type type = 1; repeated Extent dst_extents = 2; // 写到哪 optional uint64 data_offset = 3; // Blob Data 中的偏移 optional uint64 data_length = 4; repeated Extent src_extents = 5; // 从哪读(增量更新) optional uint64 src_length = 6; optional bytes data_sha256_hash = 8; enum Type { REPLACE = 0; // 直接替换 REPLACE_BZ = 1; // bzip2 压缩 REPLACE_XZ = 2; // xz 压缩 MOVE = 3; // 已废弃 BSDIFF = 4; // 二进制差分 SOURCE_COPY = 5; // 相同块直接复制 SOURCE_BSDIFF = 6; // 源二进制差分 ZERO = 10; // 清零 DISCARD = 11; // TRIM/UNMAP PUFFDIFF = 12; // puffin 差分(ELF) LZ4DIFF_BSDIFF = 13; LZ4DIFF_PUFFDIFF = 14; ZUCCHINI = 15; // Chrome 差分 } }12 种操作类型,各有各的用处:
操作类型 全量/增量 数据来源 适用场景 ──────────────────────────────────────────────────────────── REPLACE 全量 Blob 中的原始数据 小分区、完全不同的块 REPLACE_BZ 全量 bzip2 压缩数据 压缩率高但解压慢 REPLACE_XZ 全量 xz 压缩数据 压缩率最高 SOURCE_COPY 增量 无(直接复制) 新旧完全相同的块 SOURCE_BSDIFF 增量 Blob 中的差分数据 大部分文件修改 PUFFDIFF 增量 puffin 差分数据 ELF 文件优化 LZ4DIFF_BSDIFF 增量 LZ4+bsdiff 差分 压缩分区的差分 LZ4DIFF_PUFFDIFF 增量 LZ4+puffdiff 压缩分区的 ELF ZERO - 无(清零操作) 需要置零的区域 DISCARD - 无(TRIM 操作) SSD/UFS 优化 ZUCCHINI 增量 Chrome 差分数据 Android 13+增量更新时,SOURCE_COPY 占比通常最高——新旧版本大部分数据是一样的,直接复制就行,连 Blob Data 都不用。这也是为什么增量包比全量包小那么多。
Extent:块级地址
protobuf
message Extent { optional uint64 start_block = 1; optional uint64 num_blocks = 2; }Extent 就是"从第几块开始,连续几块"。分区被切成固定大小的块(通常 4KB),Extent 用块号定位。为什么不用字节偏移?因为块设备本身按块操作,文件系统 I/O 单位也是块,直接用块号省一次转换。
数据结构关系总览
看完各个结构,把它们串起来。从 Payload 二进制文件到最终的块级执行,数据是这样流转的:

DeltaArchiveManifest 包含多个 PartitionUpdate,每个分区包含多个 InstallOperation,每个操作通过 Extent 定址,通过 data_offset 引用 Blob Data。
简单说:一个 Manifest → 多个分区 → 多个操作 → 多个块地址 + 数据引用。
下面这张图更详细地展示了 protobuf 消息的字段层次:

两层签名:为什么要分两层

OTA Payload 有两层签名:
第一层:Metadata Signature ├─ 签名对象:Header + Manifest ├─ 位置:紧接在 Manifest 之后 └─ 作用:防止 Manifest 被篡改 第二层:Payload Signature ├─ 签名对象:Header + Manifest + Blob Data ├─ 位置:文件末尾 └─ 作用:防止 Blob Data 被篡改为什么要分两层?
OTA 包动不动几百 MB 甚至几 GB,下载到一半断了很常见。断点续传时,已下载的部分需要校验——但如果签名只在文件末尾,你得下完整个包才能验证。
有了 Metadata Signature,下载一开始就能验证 Manifest。Manifest 里有每个 operation 的数据 SHA256,所以每写一块都能单独校验。
这就是"边下边验"。
验证流程:
1. 读 Header → 解析 metadata_signature_size 2. 读 Manifest(manifest_size 字节) 3. 读 Metadata Signature(metadata_signature_size 字节) 4. 公钥验证签名 → 失败就中止 5. 解析 Manifest → 拿到每个 operation 的信息 6. 下载 Blob Data → 每块数据用 SHA256 校验 7. 下载完 → 验证 Payload Signature源码在 payload_verifier.cc:
cpp
ErrorCode PayloadVerifier::VerifyPayload( const brillo::Blob& payload, const brillo::Blob& metadata_signature, const brillo::Blob& payload_signature) { brillo::Blob metadata( payload.begin(), payload.begin() + kDeltaMagic.size() + kDeltaPayloadHeaderSize + manifest_size); if (!VerifySignature(metadata, metadata_signature)) return ErrorCode::kDownloadMetadataSignatureMismatch; if (!payload_signature.empty()) { if (!VerifySignature(payload, payload_signature)) return ErrorCode::kDownloadPayloadSignatureMismatch; } return ErrorCode::kSuccess; }为什么用 protobuf 而不是 JSON
Google 选 protobuf,理由很实际:
1.体积小——二进制编码,比 JSON 小 3-10 倍。OTA 包动辄几百 MB,Manifest 本身得尽量小。
2.解析快——不需要字符串解析,直接内存映射。
3.向前兼容——新增字段老版本解析器直接忽略,不会崩。Android 10 加的动态分区信息,老设备照样能解析。
4.schema 严格——`.proto` 文件就是文档,不会有字段名拼错这种低级错误。
代价是可读性差。你不能 cat 一个 payload 文件就看懂 Manifest,得用工具:
bash
dd if=payload.bin bs=1 skip=24 count=<manifest_size> 2>/dev/null | \ protoc --decode=DeltaArchiveManifest update_metadata.proto小结
Payload 格式看起来就是"Header + protobuf",但每个设计选择都有它的道理:
• 两层签名——Metadata Signature 让断点续传时能提前验证,不用等下完整个包
• 操作抽象——12 种操作类型覆盖全量、增量、清零所有场景
• 块级寻址——Extent 用块号不用字节偏移,和底层 I/O 对齐
• protobuf 选型——二进制编码、向前兼容、schema 严格
每个选择都对应着 OTA 场景下的真实约束。
下一篇聊 ActionProcessor——这条流水线到底怎么运转的,为什么能支持暂停、恢复、取消,为什么一个 Action 失败不会拖垮整个系统。
*源码基于 AOSP system/update_engine,proto 定义在 update_metadata.proto。*
夜雨聆风