A/B 系统是 Android 7.0 引入的一项重大架构变革,它彻底改变了 OTA 升级的体验。本文将从分区设计、槽位切换、回退机制到代码实现,全面剖析这套机制。
一、为什么要引入 A/B?
传统单分区 OTA 的痛点
老式 Android 设备使用单分区:
boot recovery system vendor userdata
升级流程:
1. 下载 OTA 包
2. 重启进 recovery
3. recovery 解压补丁,慢慢覆盖 system 分区
4. 重启回到正常系统
5. 用户在 "升级中..." 界面盯 10 分钟
痛点:
- 升级过程中设备完全无法使用
- 若升级中途断电 → 设备变砖,只能售后
- system 必须是可写的,后续无法做 dm-verity 强校验
A/B 系统的解法
把 system, vendor, boot, dtbo 等关键分区都做成两份:
boot_a boot_b
system_a system_b
vendor_a vendor_b
...
任意时刻只有一个槽位(slot)是"当前激活"的,另一个槽位用来后台升级。
升级流程变成:
1. 用户正常用手机 (运行 slot A)
2. 下载 OTA 包 (运行 slot A)
3. 把新版本写入 slot B (运行 slot A,用户感觉不到)
4. 用户晚上重启 -> 切到 slot B
5. 启动失败?自动回退 slot A
优点:
- 升级几乎是后台静默的
- 失败自动回退,几乎不可能变砖
- system 可以严格只读,启用完整 dm-verity
二、A/B 槽位的核心数据结构
A/B 元数据存在 misc 分区的偏移 2048 字节处,定义如下:
c
// bootable/recovery/bootloader_message/include/bootloader_message/bootloader_message.h
struct bootloader_control {
char slot_suffix[4]; // "_a\0\0" 或 "_b\0\0"
uint32_t magic; // 0x42414342 ('BCAB' little endian)
uint8_t version; // 1
uint8_t nb_slot : 3; // slot 数 (通常 2)
uint8_t recovery_tries_remaining : 3;
uint8_t merge_status : 3; // VAB 用,后面讲
uint8_t reserved0[1];
struct slot_metadata {
uint8_t priority : 4; // 0~15,越大越优先
uint8_t tries_remaining : 3; // 启动尝试次数(失败递减)
uint8_t successful_boot : 1; // 是否曾成功启动过
uint8_t verity_corrupted : 1; // dm-verity 是否检测到损坏
uint8_t reserved : 7;
} slot_info[4];
uint8_t reserved1[8];
uint32_t crc32_le;
} __attribute__((packed));
每个槽位有 3 个关键字段:
- priority --- 决定优先启动哪个槽。新刷的槽 priority=15,旧槽 priority=14
- tries_remaining --- 剩余尝试次数,通常初始为 7
- successful_boot --- 是否曾经完整启动过(由 update_engine 标记)
三、Bootloader 选槽的逻辑
Bootloader 在每次开机时根据 BCB 决定从哪个槽启动:
c
int select_active_slot(struct bootloader_control *bc) {
int active = -1;
uint8_t best_priority = 0;
for (int i = 0; i < bc->nb_slot; i++) {
struct slot_metadata *s = &bc->slot_info[i];
// 跳过坏槽
if (s->priority == 0) continue;
if (s->tries_remaining == 0 && !s->successful_boot) {
s->priority = 0; // 标记为不可启动
continue;
}
// 选优先级最高的
if (s->priority > best_priority) {
best_priority = s->priority;
active = i;
}
}
if (active < 0) {
// 两个槽都坏了 -> 紧急 fallback
return -1;
}
// 减少尝试次数(尚未确认启动成功前)
if (!bc->slot_info[active].successful_boot) {
bc->slot_info[active].tries_remaining--;
write_back_bcb(bc);
}
return active;
}
启动后,Kernel cmdline 会带上 androidboot.slot_suffix=_a 或 _b,内核挂载 system 时根据这个选 /dev/block/by-name/system_a 还是 system_b。
四、update_engine 的工作流程
update_engine 是负责执行 OTA 的守护进程,源码在 system/update_engine/。
流程图
+----------------+
| GMS / Settings | 用户点"立即下载"
+-------+--------+
|
v
+----------------+
| update_engine |
+-------+--------+
|
| 1. 下载 payload.bin
v
+----------------+
| DeltaPerformer | 解析 ops list,逐 op 应用
+-------+--------+
|
v 2. 写入 inactive slot
+----------------+
| block device | system_b, boot_b, vendor_b ...
+----------------+
|
| 3. 标记 slot B 为 active 但未成功
v
+----------------+
| bootloader | 下次启动选 slot B
+----------------+
payload.bin 结构
OTA 包里关键文件就是 payload.bin,它本质是 protobuf 编码的操作流:
+-----------------------+
| Magic "CrAU" |
+-----------------------+
| File format version | uint64
+-----------------------+
| Manifest size | uint64
+-----------------------+
| Metadata signature sz | uint32
+-----------------------+
| Manifest (protobuf) | 描述每个分区的操作
+-----------------------+
| Metadata signature | RSA 签名
+-----------------------+
| Data blobs | 实际数据 (按 op 拼接)
+-----------------------+
| Payload signature |
+-----------------------+
Manifest 里每个分区都有一组 InstallOperation:
protobuf
message InstallOperation {
enum Type {
REPLACE = 0; // 完整替换
REPLACE_BZ = 1; // bzip2 压缩替换
REPLACE_XZ = 8; // xz 压缩替换
SOURCE_COPY = 4; // 从源分区复制
SOURCE_BSDIFF= 5; // 差分补丁
BROTLI_BSDIFF= 10; // brotli 压缩的 bsdiff
ZUCCHINI = 13; // Zucchini 二进制差分
...
}
required Type type = 1;
optional uint64 data_offset = 2;
optional uint64 data_length = 3;
repeated Extent src_extents = 4;
repeated Extent dst_extents = 5;
}
关键函数:DeltaPerformer::PerformOperation
简化后的伪代码:
cpp
bool DeltaPerformer::PerformOperation(const InstallOperation& op,
const uint8_t* data) {
switch (op.type()) {
case REPLACE:
return WriteToTarget(op.dst_extents(), data, op.data_length());
case REPLACE_XZ: {
std::vector<uint8_t> decompressed;
XzDecompress(data, op.data_length(), &decompressed);
return WriteToTarget(op.dst_extents(),
decompressed.data(),
decompressed.size());
}
case SOURCE_COPY: {
// 从当前 slot 读 -> 写到目标 slot
std::vector<uint8_t> buf(GetExtentsSize(op.src_extents()));
ReadFromSource(op.src_extents(), buf.data());
return WriteToTarget(op.dst_extents(), buf.data(), buf.size());
}
case SOURCE_BSDIFF:
case BROTLI_BSDIFF: {
// 增量补丁:src(老版本) + patch -> dst(新版本)
return ApplyBsdiffPatch(op.src_extents(),
data,
op.data_length(),
op.dst_extents());
}
case ZUCCHINI:
return ApplyZucchini(op.src_extents(),
data,
op.data_length(),
op.dst_extents());
}
return false;
}
差分补丁的妙处:OTA 包从几百 MB 缩减到几十 MB,而且只需要拷贝/读取的块都不会重新写入,延长闪存寿命。
五、确认升级成功的标记
启动到新槽后,系统会运行一系列健康检查。如果一切正常,会调用:
cpp
// system/update_engine/common/boot_control_interface.h
class BootControlInterface {
virtual bool MarkBootSuccessful() = 0;
};
最终落到 Bootloader HAL 的 BCB 修改:
cpp
bool BootControl::MarkBootSuccessfulAsync(Slot slot,
std::function<void(bool)> callback) {
struct bootloader_control bc;
LoadBootloaderControl(&bc);
bc.slot_info[slot].successful_boot = 1;
bc.slot_info[slot].tries_remaining = 0; // 不再需要尝试
bc.crc32_le = ComputeCrc32(&bc, sizeof(bc) - 4);
SaveBootloaderControl(&bc);
callback(true);
return true;
}
如果新槽连续启动失败,Bootloader 自动减少 tries_remaining,降为 0 后回滚到旧槽 ------ 这就是无感回退。
六、VAB:虚拟 A/B(Virtual A/B)
Android 11 引入 Virtual A/B,目的是降低 A/B 的存储成本(因为传统 A/B 需要双份 system/vendor,占用很大)。
核心思想
利用 dm-snapshot(基于 COW)在一份物理 system 上虚拟出两个槽位。升级时,新数据写入 COW 区域,启动时通过 dm 层叠合并。
+--------------------+
| dm-snapshot |
+----+----------+----+
| |
v v
+--------+ +--------+
| base | | COW |
| (old) | | (diff) |
+--------+ +--------+
启动新版本时,内核构造一个虚拟块设备 = base + COW。
merge_status 状态机
VAB 比传统 A/B 多了一个"合并"步骤(把 COW 数据合并回 base):
c
enum merge_status {
NONE = 0, // 无升级中
UNKNOWN = 1,
SNAPSHOTTED = 2, // 已写入快照,等待启动
MERGING = 3, // 启动后正在后台合并
CANCELLED = 4,
};
启动后由 snapshotctl merge 在后台慢慢把 COW 数据应用回 base 分区,这个过程对用户透明。
七、动手:查看当前 A/B 状态
bash
adb shell
# 当前 slot suffix
getprop ro.boot.slot_suffix
# _a
# A/B 是否可用
getprop ro.build.ab_update
# true
# 各分区当前 slot
ls /dev/block/by-name | grep system
# 查看 update_engine 状态
dumpsys update_engine
# 通过 bootctl 工具
bootctl get-current-slot # 0 或 1
bootctl get-active-boot-slot
bootctl is-slot-bootable 0
bootctl is-slot-bootable 1
bootctl is-slot-marked-successful 0
输出示例:
$ bootctl get-current-slot
0
$ bootctl is-slot-bootable 0
Slot 0 is bootable
$ bootctl is-slot-bootable 1
Slot 1 is bootable
$ bootctl get-suffix 0
_a
强制切换 slot(仅用于调试)
bash
adb reboot bootloader
fastboot set_active b
fastboot reboot
八、自己手撸一个 A/B 工具(C 实现)
下面是一个简单的 C 程序,读取 misc 分区中的 BCB 并打印 slot 信息:
c
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#define BCB_OFFSET 2048
struct slot_metadata {
uint8_t priority : 4;
uint8_t tries_remaining : 3;
uint8_t successful_boot : 1;
uint8_t verity_corrupted : 1;
uint8_t reserved : 7;
} __attribute__((packed));
struct bootloader_control {
char slot_suffix[4];
uint32_t magic;
uint8_t version;
uint8_t nb_and_tries; // packed bitfield
uint8_t reserved0[2];
struct slot_metadata slot_info[4];
uint8_t reserved1[8];
uint32_t crc32_le;
} __attribute__((packed));
int main(int argc, char **argv) {
const char *path = (argc > 1) ? argv[1] : "/dev/block/by-name/misc";
int fd = open(path, O_RDONLY);
if (fd < 0) { perror("open"); return 1; }
lseek(fd, BCB_OFFSET, SEEK_SET);
struct bootloader_control bc;
if (read(fd, &bc, sizeof(bc)) != sizeof(bc)) {
perror("read");
return 1;
}
close(fd);
if (bc.magic != 0x42414342) {
fprintf(stderr, "Bad magic: 0x%08X\n", bc.magic);
return 1;
}
printf("Current slot: %s\n", bc.slot_suffix);
printf("Slot count: %u\n", bc.nb_and_tries & 0x7);
const char *names[] = {"A", "B", "C", "D"};
for (int i = 0; i < 2; i++) {
printf("\n[Slot %s]\n", names[i]);
printf(" priority : %u\n", bc.slot_info[i].priority);
printf(" tries_remaining : %u\n", bc.slot_info[i].tries_remaining);
printf(" successful_boot : %u\n", bc.slot_info[i].successful_boot);
printf(" verity_corrupted : %u\n", bc.slot_info[i].verity_corrupted);
}
return 0;
}
九、踩坑经验
- OTA 增量包不能从两个版本前升级:必须从直接上一版本升,所以厂商通常给出 full OTA 兜底
- 首次启动会非常慢:VAB 合并 COW 时,如果分区很大可能需要几分钟
- fastboot 在 A/B 设备上 :
fastboot flash boot xxx.img只会刷当前 slot,要刷另一个 slot 需要--slot=other - vbmeta 也要分槽:vbmeta_a 和 vbmeta_b 都要刷,否则会启动失败
- userdata 是共享的:不分槽,所以 OTA 升级保留用户数据
十、总结
A/B 系统是 Android 工程化思维的杰作:
- 通过简单的"双备份"思想,把"升级失败 = 变砖"的高风险操作,变成了"失败 = 回退"的常规操作
- update_engine 的增量算法配合后台 IO,让用户基本感知不到升级过程
- VAB 进一步用 dm-snapshot 缩减了存储开销,体现了 Linux 块设备子系统的强大
理解 A/B 机制,对开发 OEM 定制版本、做开机优化、排查 OTA 失败都至关重要。下篇我们继续讲 AVB(Android Verified Boot),它是 A/B 系统安全性的最后一道防线。