4. 升级包结构:升级包格式、元数据描述、签名与校验、增量包与全量包

好,咱们来聊聊升级包的结构。说实话,这是整个A/B分区方案里最容易被低估的一环。很多人觉得升级包不就是把镜像打个包嘛,有什么好讲的?嗯,我当年也这么想,直到有一次在产线上发现升级包解析失败,整批设备变砖……那教训,够我记一辈子。

一个合格的升级包,至少得包含四样东西:正确的格式、清晰的元数据、可靠的签名校验、以及合适的打包策略。缺一个,你的OTA方案就可能在某个深夜给你打电话。

4.1 升级包格式:别小看这个壳

升级包的格式,说白了就是你怎么把一堆文件组织成一个文件。常见的格式有这么几种:

格式 特点 适用场景
ZIP 通用、有CRC校验、支持压缩 Android OTA包、通用Linux
tar.gz 流式处理、适合大文件 嵌入式Linux、路由器固件
自定义二进制 紧凑、解析快、可定制 资源受限的MCU设备
sparse image Android特有、支持空洞 Android系统分区镜像

我个人习惯用ZIP格式。为什么?因为它的central directory结构让我可以在不解压的情况下,直接读取文件列表和元数据。这在升级前的校验阶段特别有用——你想想看,如果每次校验都要把整个包解压一遍,那得浪费多少时间?

我的经验: 在ZIP包的末尾追加一个自定义的footer,用来存放升级包的版本号、目标设备ID、构建时间等关键信息。这样升级程序只需要读取最后几百字节,就能决定要不要继续处理这个包。我在一个智能家居项目里用过这个技巧,把升级前的预检时间从3秒降到了0.1秒。

4.2 元数据描述:升级包的“身份证”

元数据,就是描述升级包自身信息的数据。它告诉升级程序:你是谁?你要升级谁?你包含了哪些内容?

一个典型的元数据文件(比如叫 META-INF/MANIFEST.MF)大概长这样:

ota-type: ab
ota-version: 2.1
target-device: myboard-v3
build-fingerprint: myboard/v3/4.19.123
timestamp: 2024-03-15T10:30:00Z
payload-size: 256000000
payload-hash: sha256:e3b0c44298fc1c149afbf4c8996fb924...

这里面每一行都有讲究:

  • ota-type:标明这是A/B升级包,不是传统的recovery升级包。这个字段决定了升级流程的走向。
  • target-device:防止刷错设备。我曾经见过一个同事,把手机固件刷到了平板上……嗯,结果就是屏幕显示不全。
  • payload-size / payload-hash:用于校验实际数据的完整性。注意,这里校验的是整个payload,不是单个文件。
避坑指南: 元数据一定要放在升级包的固定位置,并且要有冗余备份。我曾经遇到过一个情况,升级包下载到一半断电了,元数据区被截断,导致升级程序无法解析。后来我强制要求元数据必须同时出现在包的开头和结尾,各存一份。

4.3 签名与校验:信任的基石

升级包如果不做签名校验,那跟裸奔没什么区别。攻击者可以伪造一个恶意升级包,让你的设备变成他的肉鸡。

签名流程一般是这样的:

  1. 计算整个payload的哈希值(SHA-256或SHA-512)
  2. 用私钥对这个哈希值进行签名(RSA或ECDSA)
  3. 将签名结果和公钥证书一起打包进升级包

校验流程反过来:

  1. 从升级包中提取签名和证书
  2. 验证证书的合法性(是否由可信CA签发、是否过期)
  3. 用公钥解密签名,得到原始哈希值
  4. 重新计算payload的哈希值,比对是否一致

这里有个细节很多人会忽略:签名要覆盖元数据。为什么?因为攻击者可以篡改元数据里的payload-hash,然后替换掉真正的payload。如果你只校验payload而不校验元数据,这个漏洞就存在。

我踩过的坑: 有一次,我发现升级包校验通过了,但刷进去的系统就是起不来。查了两天才发现,是元数据里的分区大小写错了——把system写成了System。签名校验只检查了payload的哈希,没检查元数据本身。从那以后,我要求签名必须覆盖整个升级包(包括元数据),而不仅仅是payload。

4.4 增量包与全量包:空间与时间的博弈

全量包,就是把整个分区镜像打包。简单粗暴,但体积大。增量包,只包含从旧版本到新版本的变化部分。体积小,但依赖旧版本。

什么时候用全量包?

  • 设备第一次升级(没有旧版本可依赖)
  • 跨大版本升级(比如从Android 12到Android 14)
  • 旧版本已经被破坏,无法作为基准

什么时候用增量包?

  • 小版本迭代(修复bug、安全补丁)
  • 网络带宽有限(比如2G/3G网络)
  • 用户流量敏感(不想让用户抱怨升级包太大)

增量包的核心技术是bsdiffimgdiff。它们的工作原理是:

  1. 对比新旧两个镜像,找出差异块
  2. 只记录差异部分,以及差异在旧镜像中的位置
  3. 在目标设备上,用旧镜像+差异数据,重建新镜像

举个例子:

# 生成增量包
bsdiff old_system.img new_system.img system.patch

# 应用增量包
bspatch old_system.img new_system.img system.patch
我的建议: 不要只依赖一种策略。我通常的做法是:在服务器端同时生成全量包和增量包,然后让设备根据自身情况选择。如果设备检测到当前版本与目标版本之间的差异小于30%,就下载增量包;否则下载全量包。这样既节省了带宽,又保证了兼容性。

不过,增量包有个天生的弱点:依赖链。如果用户跳过了某个中间版本,增量包就无法应用。比如用户从v1.0直接升级到v3.0,但增量包只支持v2.0→v3.0,那就尴尬了。

解决这个问题有两种思路:

  • 生成多个增量包:v1.0→v3.0、v2.0→v3.0,让设备选择匹配的版本
  • 回退到全量包:如果找不到匹配的增量包,就下载全量包

我个人倾向于第二种。因为维护多个增量包的成本太高了——你想想看,如果有10个旧版本,就要生成9个增量包,测试工作量翻倍。不如让全量包作为兜底方案。

注意: 增量包的应用过程对内存和计算资源有要求。在低端设备上,bsdiff可能需要几百MB的临时空间来存放重建后的镜像。我曾经在一个只有256MB RAM的设备上跑增量升级,结果内存不足直接OOM了。后来我改用流式处理,边读取边写入,才解决了这个问题。

好了,关于升级包的结构,咱们就聊到这里。总结一下:格式选ZIP,元数据要冗余,签名要覆盖全包,增量包和全量包搭配使用。下一章,咱们聊聊升级流程的具体实现——从下载到切换,每一步都有坑等着你。