4、增量升级的回退:增量包的依赖关系、回退链设计与版本号管理
增量升级,说白了就是只传差异部分。好处是省流量、省时间,但代价呢?回退逻辑变得复杂了。全量升级的回退很简单——把旧版本整包刷回去就行。增量不行,你没法单独回退一个补丁,因为每个增量包都建立在特定的基线版本上。
我刚开始做OTA那会儿,就吃过这个亏。当时想当然地认为增量包回退就是“反向打补丁”,结果设备变砖了。嗯,今天咱们就把这块彻底讲透。
4.1 增量包的依赖关系
增量包不是独立的。它必须知道:我从哪个版本出发?我修改了哪些文件?我依赖哪些前置条件?
举个例子,假设你的设备当前是 v1.0,你想升到 v1.2。如果直接做一个 v1.0→v1.2 的增量包,那没问题。但如果先升了 v1.1,再升 v1.2,这时候 v1.0 的增量包就不能用了——基线变了。
source_version 和 target_version。升级引擎在应用增量包之前,必须校验当前版本是否匹配 source_version。
我在项目中见过一种很实用的做法——在增量包的头部元数据里,放一个依赖清单:
// 增量包元数据示例
{
"package_type": "incremental",
"source_version": "1.0.0",
"target_version": "1.1.0",
"dependencies": [
{"version": "1.0.0", "checksum": "a1b2c3..."}
],
"files": [
{"path": "/system/lib/libfoo.so", "old_checksum": "...", "new_checksum": "..."},
{"path": "/system/bin/bar", "old_checksum": "...", "new_checksum": "..."}
]
}
为什么要放 checksum?因为光靠版本号不够。我记得有一次,客户那边私自改了系统文件,版本号没变,但内容变了。增量包打上去直接崩溃。从那以后,我坚持在依赖里加上文件校验和。
4.2 回退链设计
增量升级的回退,不能只考虑“回到上一个版本”。你得设计一条完整的回退链。
什么叫回退链?就是每个版本都知道自己能从哪些版本升上来,也能知道自己能回退到哪些版本去。
我习惯用有向图来管理这个关系。节点是版本号,边是增量包。举个例子:
看到这个图了吗?每个版本只能回退到它的直接前驱。v1.2.0 可以回退到 v1.1.0,但不能直接回退到 v1.0.0。为什么?因为 v1.2.0 的增量包只记录了 v1.1.0→v1.2.0 的差异,它不知道 v1.0.0 长什么样。
4.3 版本号管理策略
版本号不只是给人看的,更是给升级引擎做决策用的。我见过太多团队把版本号当摆设,结果回退时一团糟。
我个人推荐语义化版本号 + 内部构建号的组合方案:
| 字段 | 示例 | 含义 | 回退中的作用 |
|---|---|---|---|
| 主版本号 | 1 | 不兼容的API变更 | 主版本不同时,禁止增量回退,必须用全量包 |
| 次版本号 | 2 | 向下兼容的功能新增 | 次版本不同时,允许增量回退,但需校验依赖链 |
| 修订号 | 3 | 问题修复 | 修订号不同时,直接回退到上一个修订号 |
| 构建号 | 20241015 | 构建日期/流水号 | 用于精确匹配增量包的基线版本 |
这里有个坑——构建号必须唯一。我曾经遇到一个项目,两个不同分支的构建号撞了,结果升级引擎以为版本相同,跳过了增量包,导致设备停留在旧版本。从那以后,我强制要求构建号包含分支标识和时间戳。
1.2.3-4 表示当前版本距离基线版本有 4 次增量升级。这样升级引擎可以快速判断:如果回退深度超过某个阈值(比如 5),就自动切换成全量升级,避免增量链过长导致性能问题。
4.4 回退链的存储与校验
回退链不能只存在服务器上。设备本地必须维护一份回退链的副本,否则断网后设备就不知道该怎么回退了。
我建议在设备的 recovery 分区或 OTA 元数据分区里,维护一个回退链表:
// 设备本地回退链表(简化示例)
[
{"version": "1.0.0", "checksum": "abc...", "type": "full"},
{"version": "1.1.0", "checksum": "def...", "type": "incremental", "from": "1.0.0"},
{"version": "1.2.0", "checksum": "ghi...", "type": "incremental", "from": "1.1.0"},
{"version": "2.0.0", "checksum": "jkl...", "type": "full"}
]
每次升级成功后,往这个表里追加一条记录。回退时,从表尾往前遍历,找到第一个可以回退的目标版本。
校验逻辑也很关键:
- 版本号校验:目标版本的语义化版本号必须小于当前版本
- 依赖链校验:当前版本必须在目标版本的“可回退到”列表中
- 完整性校验:回退路径上的每个增量包或全量包,checksum 必须匹配
- 签名校验:所有回退包必须经过签名验证,防止篡改
4.5 回退链的剪枝与优化
回退链不能无限增长。设备存储空间有限,你不可能保留所有历史版本的增量包。
我常用的策略是“三级保留”:
- 当前版本:保留完整的全量包(用于紧急回退)
- 上一个版本:保留增量包(用于快速回退)
- 基线版本:保留全量包(用于跨版本回退)
其他中间版本,只保留元数据,不保留实际包体。如果用户需要回退到更早的版本,就触发全量回退。
嗯,这里要注意一点——剪枝操作必须在升级成功并验证通过之后进行。我见过有人为了省空间,升级还没完成就把旧包删了,结果升级失败,设备直接变砖。