Android A/B 无缝更新机制深度剖析

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;
}

九、踩坑经验

  1. OTA 增量包不能从两个版本前升级:必须从直接上一版本升,所以厂商通常给出 full OTA 兜底
  2. 首次启动会非常慢:VAB 合并 COW 时,如果分区很大可能需要几分钟
  3. fastboot 在 A/B 设备上 :fastboot flash boot xxx.img 只会刷当前 slot,要刷另一个 slot 需要 --slot=other
  4. vbmeta 也要分槽:vbmeta_a 和 vbmeta_b 都要刷,否则会启动失败
  5. userdata 是共享的:不分槽,所以 OTA 升级保留用户数据

十、总结

A/B 系统是 Android 工程化思维的杰作:

  • 通过简单的"双备份"思想,把"升级失败 = 变砖"的高风险操作,变成了"失败 = 回退"的常规操作
  • update_engine 的增量算法配合后台 IO,让用户基本感知不到升级过程
  • VAB 进一步用 dm-snapshot 缩减了存储开销,体现了 Linux 块设备子系统的强大

理解 A/B 机制,对开发 OEM 定制版本、做开机优化、排查 OTA 失败都至关重要。下篇我们继续讲 AVB(Android Verified Boot),它是 A/B 系统安全性的最后一道防线。

相关推荐
rosemary5122 小时前
SOME/IP初试
网络·网络协议·tcp/ip·someip
企客宝CRM2 小时前
2026年中小企业CRM选型指南:企客宝CRM处于什么位置?
android·算法·企业微信·rxjava·crm
不知名的老吴2 小时前
认识Python网络套接字编程
网络
Yang96113 小时前
鼎讯 SZT-2000A:铁路高速万兆网络一站式测试方案
网络
星恒讯工业路由器3 小时前
星恒讯5G工业级通信模组选型指南:接口配置、工业防护与应用场景详解
网络·物联网·5g·信息与通信
simplepeng3 小时前
我通过3个小改动将Compose重组减少了78%
android
应用市场3 小时前
Android分区表深度解析:GPT、各分区作用与布局实战
android·gpt
云边云科技_云网融合4 小时前
企业出海的 “数字丝绸之路“:SD-WAN 如何重构全球网络竞争力
大数据·运维·网络·人工智能