12、OTA包生成:ota_from_target_files脚本、payload.bin结构

说到OTA升级,很多人只关心升级包怎么刷进去。但作为一个搞过几年系统升级的工程师,我得说——OTA包是怎么生成的,才是整个链路里最容易被忽视、却又最容易出问题的一环。

我记得刚接手Android系统升级那会儿,第一次跑ota_from_target_files脚本,看着终端里刷刷刷的输出,心里还挺美。结果生成的包刷到设备上,直接卡在验证阶段。后来一查,是payload.bin里的元信息写错了。嗯,从那以后,我再也不敢小看这个脚本了。

12.1 ota_from_target_files:幕后总指挥

这个脚本,说白了就是Google提供的一个Python工具。它的输入是target_files.zip——也就是你编译出来的那个完整系统包。输出呢,就是我们最终要用的OTA升级包。

你可能会问:为什么不直接用编译产物?非要绕一圈?

原因很简单:target_files.zip里包含了编译时的完整元数据。比如每个分区的文件列表、权限、selinux上下文、甚至文件哈希。这些信息,是生成增量包和全量包的基础。

核心命令示例:

# 生成全量OTA包
ota_from_target_files \
  --block \
  --output_metadata_path payload_metadata.txt \
  target_files.zip \
  ota_full.zip

# 生成增量OTA包
ota_from_target_files \
  --block \
  --incremental_from previous_target_files.zip \
  target_files.zip \
  ota_incremental.zip

我个人习惯在生成增量包时,加上--verify参数。这会让脚本在生成完成后,自动做一次模拟升级校验。虽然多花几分钟,但能提前发现很多低级错误。

12.2 payload.bin:升级包的心脏

OTA包解压后,你会看到一个payload.bin文件。这个文件,就是升级引擎真正要解析和执行的东西。

它的结构其实不复杂,但设计得很精巧。我画了一张图,帮你快速理解:

payload.bin 结构示意图 Payload 头部 (Magic + Version + Manifest Size) Manifest (Protobuf 序列化) 包含:分区列表、操作列表、文件哈希、元数据 例如:system, vendor, product 等分区的升级操作 操作数据 (Operations) 每个操作包含:类型 (REPLACE / SOURCE_COPY / ZERO 等) 数据偏移、数据长度、目标分区、目标偏移 实际数据块紧跟在 Manifest 之后 注意:Manifest 中记录了每个操作在数据区中的偏移量,用于快速定位

从上图你能看到,payload.bin主要分三块:

  • 头部:固定格式,包含魔数、版本号、Manifest长度。我遇到过有人手动改版本号导致升级失败的情况——千万别这么干。
  • Manifest:用Protobuf序列化的元数据。这里面记录了所有分区的升级操作。说白了,就是一张“施工图纸”。
  • 操作数据:真正的二进制数据块。每个操作对应一段数据,可能是新文件内容,也可能是差分补丁。

12.3 Manifest 的详细结构

Manifest里到底写了什么?我直接贴一段解析后的内容给你看:

partitions {
  partition_name: "system"
  estimate_size: 2147483648
  operations {
    type: REPLACE
    data_offset: 4096
    data_length: 1048576
    dst_extents {
      start_block: 0
      num_blocks: 2048
    }
  }
  operations {
    type: SOURCE_COPY
    src_extents {
      start_block: 2048
      num_blocks: 1024
    }
    dst_extents {
      start_block: 2048
      num_blocks: 1024
    }
  }
  hash: "a1b2c3d4e5f6..."
}

这里有几个关键点:

  • REPLACE操作:直接用新数据覆盖目标区域。全量包基本都用这个。
  • SOURCE_COPY操作:从源分区拷贝数据到目标分区。增量包里很常见。
  • ZERO操作:把目标区域全部写0。用于擦除数据。

避坑指南:

我曾经在生成增量包时,发现某个分区总是校验失败。排查了半天,最后发现是Manifest里记录的源分区哈希和实际设备上的不一致。原因是编译时用了不同的签名密钥。所以——生成增量包时,一定要确保前后两个target_files.zip来自同一套编译环境

12.4 操作类型详解

除了上面提到的三种,还有几种操作类型值得注意:

操作类型 说明 典型场景
REPLACE 直接写入新数据 全量包、新增文件
SOURCE_COPY 从源分区拷贝 未修改的文件
ZERO 写入全0 擦除分区
BROTLI_BSDIFF Brotli压缩的差分补丁 大文件增量更新
PUFFDIFF 另一种差分算法 特定场景优化

你想想看,为什么要有这么多种操作类型?

说白了,就是为了平衡升级包大小和升级速度。REPLACE最快,但包最大。BROTLI_BSDIFF包最小,但解压和打补丁需要更多CPU时间。实际项目中,我一般会针对不同分区选择不同策略——比如system分区用差分,vendor分区用全量替换。

12.5 生成过程中的关键细节

ota_from_target_files脚本在生成payload.bin时,会做几件重要的事:

  1. 解析target_files.zip:读取每个分区的文件列表、权限、哈希。
  2. 对比新旧版本:如果是增量包,逐文件对比差异。
  3. 生成操作列表:根据差异类型,选择合适的操作。
  4. 序列化Manifest:用Protobuf编码,写入payload.bin头部之后。
  5. 写入数据块:按操作顺序,依次写入实际数据。

注意:

生成增量包时,脚本会计算每个文件的哈希值。如果源设备和目标设备的文件哈希不一致,升级就会失败。我遇到过一种情况:设备上某个文件被root工具修改过,哈希对不上,导致整个升级流程卡住。所以——OTA升级前,最好做一次分区完整性校验

12.6 实战:手动解析payload.bin

有时候,我们需要排查升级包的问题。手动解析payload.bin是个好习惯。我常用的方法是:

# 使用update_engine_client的调试模式
update_engine_client --payload file:///path/to/payload.bin \
  --manifest_only \
  --output /tmp/manifest.txt

# 或者直接用protobuf工具
protoc --decode \
  chromeos_update_engine.DeltaArchiveManifest \
  --proto_path /path/to/update_engine/proto \
  < payload.bin > manifest.txt

解析出来的Manifest文件,你可以直接查看每个分区的操作列表。如果发现某个分区有大量REPLACE操作,说明这个分区几乎全部重写了——那升级包自然会很大。

嗯,说到这里,我想起一个案例。有次我发现生成的增量包异常大,一查Manifest,发现vendor分区里有个日志文件每次编译都会变。后来在编译脚本里把这个文件排除掉,增量包直接缩小了60%。

所以我的建议是:生成OTA包后,先解析Manifest看看操作分布。这能帮你快速判断升级包是否合理。

总结一下:

ota_from_target_files脚本是OTA包生成的总指挥,payload.bin是升级引擎真正要吃的“粮食”。理解它们的结构和生成逻辑,能让你在遇到升级问题时,快速定位根因。别等到设备变砖了才去翻代码——提前把这些搞明白,能省很多事。


公众号:蓝海资料掘金营,微信deep3321