4、App分区管理:双备份(A/B)系统原理、切换策略、状态标记
双备份系统,圈里人常说的 A/B 系统,其实不是什么新概念。但真正把它用好在 OTA 升级里,坑不少。我最早接触这个,是在一个智能网关项目上。当时设备升级失败率高达 15%,用户投诉不断。后来上了 A/B 分区方案,失败率直接降到 0.5% 以下。嗯,今天咱们就聊聊这里面的门道。
4.1 双备份系统原理
说白了,A/B 系统就是给 App 准备了两套分区。一套跑着,一套等着。你想想看,升级的时候,设备还在用老系统正常工作。新固件下载到备用分区,校验通过后,下次重启才切过去。整个过程对用户来说,几乎无感。
我习惯把这两个分区叫做 slot_a 和 slot_b。它们的大小完全一样,各自包含完整的系统、App 和数据分区。结构上大概是这样的:
核心思想:任何时候,至少有一个分区是可启动的。升级失败不会导致设备变砖。
这里有个关键点:Bootloader 必须支持 A/B 切换逻辑。它得能读懂状态标记,知道该启动哪个分区。我在一个项目里见过,Bootloader 代码里硬编码了启动分区,结果 A/B 功能根本用不了。那真是欲哭无泪。
4.2 切换策略
切换策略,说白了就是「什么时候切,怎么切」。我总结下来,主流的有三种:
| 策略名称 | 触发时机 | 优点 | 缺点 |
|---|---|---|---|
| 立即切换 | 升级完成后立即重启 | 用户等待时间短 | 新版本有问题会立即暴露 |
| 延迟切换 | 下次设备重启时 | 给用户缓冲时间 | 可能忘记切换 |
| 条件切换 | 满足特定条件(如空闲、电量充足) | 风险最低 | 实现复杂 |
我个人比较推荐 条件切换 + 健康检查 的组合。具体流程是这样的:
- 新固件下载到备用分区,校验通过。
- 设置「待切换」标记,但不立即重启。
- 等待设备空闲(比如没有用户操作、网络流量低)。
- 检查电量是否充足(我一般设 30% 以上)。
- 执行重启,Bootloader 读取标记,启动新分区。
- 新系统启动后,运行自检程序。如果自检通过,提交「成功」标记。
- 如果自检失败或超时,自动回滚到旧分区。
我的经验:健康检查这一步千万别省。我曾经在一个项目里,新系统启动后网络模块初始化失败,但因为没做健康检查,设备直接「失联」了。后来加了 30 秒超时检测,问题才解决。
4.3 状态标记
状态标记是 A/B 系统的「大脑」。Bootloader 全靠它来做决策。我习惯把标记存储在 Bootloader 专用的 misc 分区 里,这样即使系统崩溃,标记也不会丢。
常用的状态标记有这些:
| 标记名称 | 值 | 含义 |
|---|---|---|
active_slot |
0 / 1 | 当前启动的是 Slot A 还是 Slot B |
boot_attempts |
0-3 | 当前分区已尝试启动的次数 |
update_pending |
true / false | 是否有待完成的升级 |
update_result |
success / failed / unknown | 上次升级的结果 |
rollback_count |
0-N | 回滚次数,防止无限回滚 |
代码实现上,我一般用一个结构体来管理:
// 状态标记结构体
typedef struct {
uint8_t active_slot; // 0: Slot A, 1: Slot B
uint8_t boot_attempts; // 启动尝试次数
bool update_pending; // 升级待处理
uint8_t update_result; // 0: unknown, 1: success, 2: failed
uint8_t rollback_count; // 回滚计数
uint32_t crc32; // 结构体校验
} ab_status_t;
// 读取状态标记
ab_status_t* get_ab_status(void) {
// 从 misc 分区读取
return (ab_status_t*)MISC_PARTITION_ADDR;
}
// 写入状态标记
void set_ab_status(ab_status_t* status) {
status->crc32 = calculate_crc32(status, sizeof(ab_status_t) - 4);
write_to_misc_partition(status, sizeof(ab_status_t));
}
注意:CRC32 校验不能省。我遇到过 flash 位翻转导致标记错误,设备反复在两个分区之间来回切换。加上校验后,一旦数据损坏,Bootloader 会使用默认值,至少保证能启动。
4.4 回滚机制
回滚是 A/B 系统的最后一道防线。我见过不少方案,回滚逻辑写得过于简单,结果越滚越乱。
我的做法是:
- 限制回滚次数:最多回滚 3 次。超过 3 次,设备进入恢复模式,等待人工干预。
- 记录回滚原因:是启动超时?还是健康检查失败?记录下来,方便远程排查。
- 保留旧分区:回滚后不立即擦除新分区。万一新版本只是偶发问题,还可以再试一次。
嗯,这里要特别提一下。回滚不是简单的「切回去就完事了」。你得考虑用户数据的一致性。比如新版本改了数据库结构,回滚后旧版本可能读不了新数据。我一般会在升级前备份用户数据,回滚时一并恢复。
4.5 实际部署建议
最后,给几个我在项目中踩过的坑,算是避坑指南吧:
- 分区大小要留余量:固件总会越写越大。我建议每个分区至少留 20% 的余量。
- Bootloader 要支持双分区:别像我一样,做到一半才发现 Bootloader 不支持。
- 测试要覆盖回滚场景:不只是测升级成功,更要测升级失败后的回滚。
- 日志要持久化:把升级日志写到独立的日志分区,方便事后分析。
一句话总结:A/B 系统不是银弹,但用好了,能让你的 OTA 升级成功率从 85% 提升到 99.5% 以上。关键是原理要懂,策略要合理,标记要可靠。