2、系统分区布局:A/B分区架构详解、bootloader与recovery分区、slot概念与切换机制

好,咱们进入第二个章节。说实话,A/B分区布局是整个无缝升级机制的物理基础。你想想看,如果没有这套分区设计,所谓的「无缝」根本无从谈起。我最早接触这个架构时,第一反应是:这不就是多花了一倍的存储空间吗?后来踩过坑才明白,这笔投资太值了。

2.1 传统分区架构的痛点

在讲A/B之前,咱们先回顾一下传统方案。传统的Android设备分区布局大概是这样的:

boot分区 → 存放内核和ramdisk
system分区 → 存放系统镜像
recovery分区 → 存放恢复模式
userdata分区 → 用户数据
cache分区 → 缓存数据
misc分区 → 杂项配置

这种布局有什么问题?我举个例子。你手机提示系统更新,下载完安装包后,需要重启进入recovery模式来刷写system分区。这个过程少说也要一两分钟,而且期间手机完全不可用。更糟糕的是,如果刷写过程中突然断电或者系统崩溃,你的手机就变砖了。

嗯,说白了,传统架构的升级是「先破坏,再重建」——先把旧系统擦掉,再写入新系统。这个窗口期非常危险。

2.2 A/B分区架构的核心思想

A/B分区架构,也叫「无缝升级」架构,它的核心思路就一句话:准备两套完整的系统分区,一套运行,一套备用

具体来说,每个关键分区都有一份副本:

分区角色 Slot A Slot B
boot boot_a boot_b
system system_a system_b
vendor vendor_a vendor_b
product product_a product_b

这里有个概念叫 slot(槽位)。每个slot就是一套完整的系统副本。设备当前正在使用的slot叫 active slot,另一个就是 inactive slot

关键点:升级时,系统在后台把新版本写入inactive slot。写入完成后,只需设置一个标记,下次重启时bootloader就会从新的slot启动。整个过程用户几乎无感知。

2.3 Bootloader与Recovery分区的角色变化

在A/B架构下,bootloader和recovery分区的职责发生了微妙的变化。

Bootloader的新任务

传统bootloader只负责加载boot分区。但在A/B架构中,它多了一个关键职责:决定从哪个slot启动

我记得第一次看bootloader源码时,发现它读取一个叫 misc 分区的数据结构,里面记录了当前active slot、升级状态、回滚次数等信息。bootloader根据这些信息做决策:

// 伪代码示意
if (misc.active_slot == 'A') {
    load_boot_from_slot('A');
} else {
    load_boot_from_slot('B');
}

这里有个坑。我曾经遇到一个案例:某款设备在升级过程中意外掉电,重启后bootloader发现两个slot都标记为「不可用」。结果设备直接进入了紧急下载模式。后来我们加了一个「回滚保护」逻辑——如果新slot启动失败,自动切回旧slot。

Recovery分区的退化

在传统架构中,recovery分区是升级的核心。但在A/B架构下,recovery分区的作用大大减弱了。为什么?因为升级操作已经移到了正在运行的系统中完成,不再需要重启进recovery。

不过recovery并没有完全消失。它现在主要用来做两件事:

  • 出厂恢复:当用户选择「恢复出厂设置」时,recovery负责擦除userdata
  • 备用升级通道:如果主系统损坏,可以通过recovery刷入完整包

个人经验:我建议在recovery分区中保留一份最小化的升级逻辑。虽然A/B架构下很少用到,但万一主系统挂了,这就是最后的救命稻草。我曾经见过某厂商砍掉了recovery的升级能力,结果一台设备OTA失败后只能返厂——这个教训挺深刻的。

2.4 Slot切换机制详解

好,接下来是重点——slot到底是怎么切换的?

整个过程分为三个阶段:

  1. 升级阶段:系统在后台将新版本写入inactive slot。写入完成后,设置一个「升级完成」标记,但此时不切换slot。
  2. 重启阶段:用户正常重启(或者系统提示重启)。bootloader读取misc分区,发现inactive slot有新的升级,于是将active slot切换为新的slot。
  3. 验证阶段:新系统启动后,会检查自身完整性。如果一切正常,就「确认」这次升级,标记为永久有效。如果启动失败,bootloader会在下次重启时自动回滚到旧slot。

这里有个细节很多人会忽略:slot切换不是立即生效的。系统在升级完成后,只是把新slot标记为「待激活」。真正的切换发生在重启时,由bootloader完成。

注意:如果新系统启动后没有执行「确认」操作,bootloader会认为这次升级失败。默认情况下,系统会在启动后几分钟内自动确认。但如果你的设备在确认前又重启了,就会触发回滚。我曾经调试过一个bug,就是因为确认逻辑被某个第三方应用阻塞了,导致每次升级都回滚——排查了整整两天。

2.5 Slot状态管理的数据结构

misc分区中维护了一个关键的数据结构,我把它简化一下:

struct slot_metadata {
    uint8_t active_slot;      // 0表示slot A,1表示slot B
    uint8_t boot_successful;  // 当前slot是否启动成功
    uint8_t retry_count;      // 剩余重试次数
    uint8_t merge_status;     // 虚拟A/B的合并状态
};

这个结构虽然简单,但每个字段都有讲究。比如 retry_count,默认是7次。也就是说,如果新系统连续7次启动失败,bootloader就会放弃这个slot,切回旧的。这个设计是为了防止「反复横跳」——你想想看,如果每次启动都失败又回滚,设备不就成砖了吗?

2.6 虚拟A/B分区(Virtual A/B)

最后提一下虚拟A/B。这是Google在Android 11引入的改进方案。说白了,它解决了物理A/B的一个痛点:存储空间浪费

物理A/B需要两套完整的分区,对于128GB的设备来说,光system分区就要占掉十几GB。虚拟A/B的做法是:只保留一份完整数据,另一份用「快照」的方式记录差异。

具体原理是这样的:

  • 升级时,系统创建一个system分区的写时复制(COW)快照
  • 新版本的数据只写入变化的部分,未变化的部分直接映射到原分区
  • 升级完成后,通过「合并」操作将快照数据写回原分区

嗯,这个方案我实际测试过,确实能节省30%-50%的升级空间。但代价是合并操作需要在后台进行,如果合并过程中断电,恢复逻辑会复杂一些。

避坑指南:我曾经在一个低端设备上部署虚拟A/B,发现合并操作导致系统卡顿。后来定位到是I/O性能瓶颈——COW快照的随机写入太频繁了。解决方案是调整合并策略,只在设备空闲时执行合并。这个经验告诉我:技术方案再好,也要考虑硬件底子。

好了,这一章的内容就到这里。分区布局是A/B升级的骨架,理解了它,后面的升级流程、回滚机制就顺理成章了。下一章我们聊聊升级包的生成和签名——那又是另一番天地。