4. 升级包结构:升级包格式、元数据描述、签名与校验、增量包与全量包
好,咱们来聊聊升级包的结构。说实话,这是整个A/B分区方案里最容易被低估的一环。很多人觉得升级包不就是把镜像打个包嘛,有什么好讲的?嗯,我当年也这么想,直到有一次在产线上发现升级包解析失败,整批设备变砖……那教训,够我记一辈子。
一个合格的升级包,至少得包含四样东西:正确的格式、清晰的元数据、可靠的签名校验、以及合适的打包策略。缺一个,你的OTA方案就可能在某个深夜给你打电话。
4.1 升级包格式:别小看这个壳
升级包的格式,说白了就是你怎么把一堆文件组织成一个文件。常见的格式有这么几种:
| 格式 | 特点 | 适用场景 |
|---|---|---|
| ZIP | 通用、有CRC校验、支持压缩 | Android OTA包、通用Linux |
| tar.gz | 流式处理、适合大文件 | 嵌入式Linux、路由器固件 |
| 自定义二进制 | 紧凑、解析快、可定制 | 资源受限的MCU设备 |
| sparse image | Android特有、支持空洞 | Android系统分区镜像 |
我个人习惯用ZIP格式。为什么?因为它的central directory结构让我可以在不解压的情况下,直接读取文件列表和元数据。这在升级前的校验阶段特别有用——你想想看,如果每次校验都要把整个包解压一遍,那得浪费多少时间?
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 签名与校验:信任的基石
升级包如果不做签名校验,那跟裸奔没什么区别。攻击者可以伪造一个恶意升级包,让你的设备变成他的肉鸡。
签名流程一般是这样的:
- 计算整个payload的哈希值(SHA-256或SHA-512)
- 用私钥对这个哈希值进行签名(RSA或ECDSA)
- 将签名结果和公钥证书一起打包进升级包
校验流程反过来:
- 从升级包中提取签名和证书
- 验证证书的合法性(是否由可信CA签发、是否过期)
- 用公钥解密签名,得到原始哈希值
- 重新计算payload的哈希值,比对是否一致
这里有个细节很多人会忽略:签名要覆盖元数据。为什么?因为攻击者可以篡改元数据里的payload-hash,然后替换掉真正的payload。如果你只校验payload而不校验元数据,这个漏洞就存在。
我踩过的坑: 有一次,我发现升级包校验通过了,但刷进去的系统就是起不来。查了两天才发现,是元数据里的分区大小写错了——把system写成了System。签名校验只检查了payload的哈希,没检查元数据本身。从那以后,我要求签名必须覆盖整个升级包(包括元数据),而不仅仅是payload。
4.4 增量包与全量包:空间与时间的博弈
全量包,就是把整个分区镜像打包。简单粗暴,但体积大。增量包,只包含从旧版本到新版本的变化部分。体积小,但依赖旧版本。
什么时候用全量包?
- 设备第一次升级(没有旧版本可依赖)
- 跨大版本升级(比如从Android 12到Android 14)
- 旧版本已经被破坏,无法作为基准
什么时候用增量包?
- 小版本迭代(修复bug、安全补丁)
- 网络带宽有限(比如2G/3G网络)
- 用户流量敏感(不想让用户抱怨升级包太大)
增量包的核心技术是bsdiff或imgdiff。它们的工作原理是:
- 对比新旧两个镜像,找出差异块
- 只记录差异部分,以及差异在旧镜像中的位置
- 在目标设备上,用旧镜像+差异数据,重建新镜像
举个例子:
# 生成增量包
bsdiff old_system.img new_system.img system.patch
# 应用增量包
bspatch old_system.img new_system.img system.patch
不过,增量包有个天生的弱点:依赖链。如果用户跳过了某个中间版本,增量包就无法应用。比如用户从v1.0直接升级到v3.0,但增量包只支持v2.0→v3.0,那就尴尬了。
解决这个问题有两种思路:
- 生成多个增量包:v1.0→v3.0、v2.0→v3.0,让设备选择匹配的版本
- 回退到全量包:如果找不到匹配的增量包,就下载全量包
我个人倾向于第二种。因为维护多个增量包的成本太高了——你想想看,如果有10个旧版本,就要生成9个增量包,测试工作量翻倍。不如让全量包作为兜底方案。
好了,关于升级包的结构,咱们就聊到这里。总结一下:格式选ZIP,元数据要冗余,签名要覆盖全包,增量包和全量包搭配使用。下一章,咱们聊聊升级流程的具体实现——从下载到切换,每一步都有坑等着你。