Android Recovery 模式工作原理与定制实战

Recovery 是 Android 的"救命系统",负责 OTA 升级、恢复出厂、用户数据加密管理。本文剖析 Recovery 的架构、启动流程、与主系统的通信机制,并演示如何修改并构建一个自定义 Recovery。

一、Recovery 到底是什么?

很多人以为 Recovery 是 Android 系统的一个"模式",其实它是一个完全独立的 Linux 系统:有自己的 Kernel(实际上和主系统共享一个 boot 镜像的内核)、自己的 ramdisk、自己的 init、自己的应用。

它和主系统的关系类似:

复制代码
PC 主系统  <->  PE 启动盘
Android   <->  Recovery

Recovery 启动时只挂载最小化的文件系统,不启动 Android 框架(没有 Java VM、Zygote、SystemServer),纯 C/C++ 应用,占用资源极小。


二、Recovery 的两种存在形式

1. 传统形式:独立 recovery 分区

复制代码
分区表:
boot       (Kernel + 主系统 ramdisk)
recovery   (Kernel + recovery ramdisk)

启动时 Bootloader 根据按键 / BCB 决定加载哪个 boot image。

2. 现代形式:合并到 boot 中(A/B 设备)

A/B 设备砍掉了独立 recovery 分区,把 recovery 的内容融合进 boot 的 ramdisk:

复制代码
boot.img
├── kernel
└── ramdisk
    ├── /init
    ├── /system/bin/recovery
    ├── /sbin/...
    └── /etc/recovery.fstab

启动到主系统还是 recovery,看 Kernel cmdline:

复制代码
# 主系统启动
androidboot.force_normal_boot=1

# Recovery 启动
(不带 force_normal_boot)

init 在第一阶段读到这个参数后,执行不同的 init.rc


三、Recovery 的目录结构(精简版)

复制代码
/
├── init                       (静态链接的 init 二进制)
├── init.recovery.${hw}.rc     (硬件相关 init 脚本)
├── etc/
│   ├── recovery.fstab        (Recovery 用的 fstab)
│   └── recovery-resource.dat
├── sbin/
│   ├── recovery              (主程序)
│   ├── adbd                  (USB adb 守护进程)
│   └── busybox               (基础命令)
├── res/
│   ├── images/               (UI 图片)
│   └── recovery.cfg
├── system/
│   └── bin/
└── tmp/                      (临时挂载点)

recovery.fstab 描述了 Recovery 需要挂载的分区:

复制代码
# <src>            <mnt_point> <type>  <mnt_flags>  <fs_mgr_flags>
/dev/block/by-name/system     /system  ext4  ro,barrier=1   wait,first_stage_mount,logical
/dev/block/by-name/userdata   /data    f2fs  defaults       wait,check,formattable,quota
/dev/block/by-name/cache      /cache   ext4  defaults       wait,check
/dev/block/by-name/misc       /misc    emmc  defaults       defaults
/dev/block/by-name/metadata   /metadata f2fs defaults       wait,formattable

四、Recovery 主程序的核心流程

源码在 bootable/recovery/,核心入口在 recovery_main.cpp:

cpp 复制代码
int main(int argc, char** argv) {
    // 1. 重定向 stdio 到 last_log
    redirect_stdio("/tmp/recovery.log");

    // 2. 启动 logd 服务
    static constexpr struct option OPTIONS[] = {
        { "locale",  required_argument, NULL, 0 },
        { "fastboot",no_argument,       NULL, 0 },
        { 0, 0, 0, 0 },
    };

    // 3. 加载 fstab
    auto fstab = LoadFstab();

    // 4. 解析启动参数(来自 BCB)
    std::vector<std::string> args = get_args(argc, argv);

    // 5. 初始化 Device 抽象
    auto device = make_device();
    auto ui = device->GetUI();
    ui->Init(locale);

    // 6. 进入主循环
    InstallResult status = INSTALL_NONE;

    if (update_package != nullptr) {
        // OTA 包安装
        status = install_package(update_package, ...);
    } else if (should_wipe_data) {
        wipe_data(device);
    } else if (should_wipe_cache) {
        wipe_cache(device);
    } else if (should_wipe_ab) {
        wipe_ab_device();
    } else {
        // 进入交互式菜单
        status = prompt_and_wait(device, retry_count);
    }

    // 7. 写回结果到 BCB
    finish_recovery();

    // 8. 重启
    reboot("normal");
    return 0;
}

prompt_and_wait 交互菜单

cpp 复制代码
InstallResult prompt_and_wait(Device* device, int retry_count) {
    static const char* HEADERS[] = {
        "Android Recovery",
        "Use volume up/down to move highlight;",
        "power button to select.",
        nullptr,
    };

    static const char* ITEMS[] = {
        "Reboot system now",
        "Apply update from ADB",
        "Apply update from SD card",
        "Wipe data/factory reset",
        "Wipe cache partition",
        "Mount /system",
        "View recovery logs",
        "Power off",
        nullptr,
    };

    while (true) {
        int chosen = get_menu_selection(HEADERS, ITEMS, ...);

        switch (chosen) {
            case 0: return INSTALL_SUCCESS;
            case 1: apply_from_adb(device); break;
            case 2: apply_from_sdcard(device); break;
            case 3: wipe_data(device); break;
            case 4: wipe_cache(device); break;
            case 5: mount("/system"); break;
            case 6: choose_recovery_file(device); break;
            case 7: reboot("poweroff"); break;
        }
    }
}

五、与主系统的通信:BCB(Bootloader Control Block)

我们在前文讲过,misc 分区存放 BCB。Recovery 主要靠 BCB 与外界沟通:

主系统触发 Recovery

cpp 复制代码
// frameworks/base/services/core/java/com/android/server/RecoverySystem.java
public static void rebootWipeUserData(Context context, ...) {
    bootCommand(context, "--wipe_data", "--reason=" + reason);
}

private static void bootCommand(Context context, String... args) {
    File RECOVERY_DIR = new File("/cache/recovery");
    File COMMAND_FILE = new File(RECOVERY_DIR, "command");

    StringBuilder command = new StringBuilder();
    for (String arg : args) {
        command.append(arg).append("\n");
    }
    Files.write(command.toString().getBytes(), COMMAND_FILE);

    // 触发 reboot 到 recovery
    SystemProperties.set("sys.powerctl", "reboot,recovery");
}

Recovery 启动后读命令

cpp 复制代码
std::vector<std::string> get_args(int argc, char** argv) {
    std::vector<std::string> args;

    // 1. 优先从 BCB 读 (Bootloader 写入)
    bootloader_message boot;
    if (read_bootloader_message(&boot)) {
        if (boot.command[0] != 0) {
            // boot.recovery 中是用 '\n' 分隔的命令列表
            std::vector<std::string> tokens = Split(boot.recovery, "\n");
            args.insert(args.end(), tokens.begin(), tokens.end());
        }
    }

    // 2. 从 /cache/recovery/command 读
    if (args.empty()) {
        std::string content;
        if (ReadFileToString("/cache/recovery/command", &content)) {
            std::vector<std::string> tokens = Split(content, "\n");
            args.insert(args.end(), tokens.begin(), tokens.end());
        }
    }

    return args;
}

退出时清理 BCB

cpp 复制代码
void finish_recovery() {
    // 把 BCB 清掉,下次启动直接进主系统
    bootloader_message boot = {};
    write_bootloader_message(&boot);

    // 删除 /cache 中的命令文件
    unlink("/cache/recovery/command");
}

六、OTA 安装的核心:install_package

Recovery 最重要的工作就是安装 OTA 包。流程:

cpp 复制代码
InstallResult install_package(const std::string& path, ...) {
    // 1. 校验 ZIP 签名 (PKCS#7)
    if (!verify_package(path, key_path)) {
        return INSTALL_CORRUPT;
    }

    // 2. 打开 ZIP
    ZipArchiveHandle zip;
    if (OpenArchive(path.c_str(), &zip) != 0) {
        return INSTALL_CORRUPT;
    }

    // 3. 提取并执行 META-INF/com/google/android/update-binary
    int pipefd[2];
    pipe(pipefd);

    pid_t pid = fork();
    if (pid == 0) {
        // 子进程
        close(pipefd[0]);
        char status_fd[8];
        snprintf(status_fd, sizeof(status_fd), "%d", pipefd[1]);

        execl("/tmp/update-binary",
              "update-binary",
              "3",         // version
              status_fd,
              path.c_str(),
              nullptr);
        exit(1);
    }

    // 父进程读 update-binary 的进度
    close(pipefd[1]);
    FILE* f = fdopen(pipefd[0], "r");
    char buf[256];
    while (fgets(buf, sizeof(buf), f)) {
        // 解析 "ui_print xxx", "progress xxx", "set_progress xxx"
        handle_progress(buf);
    }

    int status;
    waitpid(pid, &status, 0);
    return WEXITSTATUS(status) == 0 ? INSTALL_SUCCESS : INSTALL_ERROR;
}

update-binary 协议

update-binary 通过文件描述符 3 与 Recovery 主进程通信,简单的文本协议:

复制代码
ui_print Hello world!\n
progress 0.5 30\n        (进度条 50%,持续 30 秒动画)
set_progress 0.75\n      (进度条 75%)
firmware hboot hboot.img\n
clear_display\n

七、Recovery 的 UI 系统

Recovery UI 是个独立的图形系统,不依赖 SurfaceFlinger / WindowManager,直接操作 framebuffer / DRM。

核心类:RecoveryUI

cpp 复制代码
// bootable/recovery/recovery_ui/include/recovery_ui/ui.h
class RecoveryUI {
public:
    virtual void Init(const std::string& locale) = 0;
    virtual void SetBackground(Icon icon) = 0;
    virtual void SetProgress(float fraction) = 0;
    virtual void Print(const char* fmt, ...) = 0;
    virtual int WaitKey() = 0;
    virtual void ShowText(bool visible) = 0;
};

具体实现 ScreenRecoveryUI 直接操作 framebuffer:

cpp 复制代码
void ScreenRecoveryUI::draw_screen_locked() {
    if (!show_text) {
        // 显示进度条 + logo
        draw_background_locked();
        draw_foreground_locked();
    } else {
        // 显示文字菜单
        SetColor(MENU);
        // ...
        for (int i = 0; i < menu_items; i++) {
            if (i == selected) {
                DrawHighlightBar(0, y, width, char_height);
                SetColor(MENU_SEL_BG);
            }
            gr_text(menu_font, x, y + baseline, items[i].text, 1);
        }
    }
}

void ScreenRecoveryUI::update_screen_locked() {
    draw_screen_locked();
    gr_flip();   // 把 framebuffer 内容显示到屏幕
}

gr_flip() 内部通过 ioctl 通知 DRM 切换显示缓冲区:

c 复制代码
int gr_flip(void) {
    gr_draw = gr_backup_disp_buffer(gr_draw);

    struct fb_var_screeninfo vi;
    ioctl(fd, FBIOGET_VSCREENINFO, &vi);
    vi.yoffset = (current_buffer ? vi.yres : 0);
    ioctl(fd, FBIOPAN_DISPLAY, &vi);

    current_buffer = !current_buffer;
    return 0;
}

八、定制 Recovery 实战

如果想给设备做一个自定义 Recovery,通常要做几件事:

1. 准备源码

bash 复制代码
# 拉 AOSP 源码
repo init -u https://android.googlesource.com/platform/manifest -b android-13.0.0_r80
repo sync

# 配置自己的设备
source build/envsetup.sh
lunch <your_device>-userdebug

2. 修改设备 Device 类

cpp 复制代码
// device/yourcompany/yourdevice/recovery/recovery_ui.cpp
#include "recovery_ui/device.h"
#include "recovery_ui/screen_ui.h"

class YourDevice : public Device {
public:
    YourDevice(RecoveryUI* ui) : Device(ui) {}

    bool PostWipeData() override {
        // 擦数据后做点啥
        ResetCustomerSettings();
        return true;
    }

    BuiltinAction InvokeMenuItem(size_t menu_position) override {
        // 添加自定义菜单项
        if (menu_position == YOUR_CUSTOM_ITEM) {
            DoSomethingCool();
            return NO_ACTION;
        }
        return Device::InvokeMenuItem(menu_position);
    }
};

Device* make_device() {
    return new YourDevice(new ScreenRecoveryUI);
}

3. 修改 BoardConfig.mk

makefile 复制代码
# device/yourcompany/yourdevice/BoardConfig.mk
TARGET_RECOVERY_UI_LIB := librecovery_ui_yourdevice
TARGET_RECOVERY_FSTAB := device/yourcompany/yourdevice/recovery.fstab

# 启用 fastbootd
TARGET_USERIMAGES_USE_F2FS := true
BOARD_USES_RECOVERY_AS_BOOT := true
BOARD_BUILD_SYSTEM_ROOT_IMAGE := false

# UI 主题
TARGET_RECOVERY_PIXEL_FORMAT := "RGBX_8888"

4. 编译

bash 复制代码
make bootimage -j$(nproc)
# 或者只编译 recovery (传统 boot/recovery 分离的设备)
make recoveryimage -j$(nproc)

输出在 out/target/product/<device>/:

  • boot.img --- A/B 设备
  • recovery.img --- 传统设备

5. 刷写

bash 复制代码
adb reboot bootloader

# A/B 设备
fastboot flash boot boot.img

# 传统设备
fastboot flash recovery recovery.img

九、minimal recovery UI 代码示例(脱离 AOSP)

下面是一个最简化的 framebuffer Recovery UI 主循环,展示原理:

c 复制代码
#include <stdio.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <linux/fb.h>
#include <linux/input.h>
#include <sys/ioctl.h>
#include <sys/mman.h>

struct fb_ctx {
    int fd;
    uint8_t *pixels;
    int width, height, stride;
};

static int fb_init(struct fb_ctx *fb) {
    fb->fd = open("/dev/graphics/fb0", O_RDWR);
    if (fb->fd < 0) return -1;

    struct fb_var_screeninfo vi;
    struct fb_fix_screeninfo fi;
    ioctl(fb->fd, FBIOGET_VSCREENINFO, &vi);
    ioctl(fb->fd, FBIOGET_FSCREENINFO, &fi);

    fb->width  = vi.xres;
    fb->height = vi.yres;
    fb->stride = fi.line_length;

    size_t size = fb->stride * fb->height;
    fb->pixels = mmap(0, size, PROT_READ|PROT_WRITE, MAP_SHARED, fb->fd, 0);
    return (fb->pixels == MAP_FAILED) ? -1 : 0;
}

static void fb_fill(struct fb_ctx *fb, uint32_t color) {
    uint32_t *p = (uint32_t*)fb->pixels;
    int count = fb->stride * fb->height / 4;
    for (int i = 0; i < count; i++) p[i] = color;
}

static int read_key(int input_fd) {
    struct input_event ev;
    while (read(input_fd, &ev, sizeof(ev)) == sizeof(ev)) {
        if (ev.type == EV_KEY && ev.value == 1) {  // key down
            return ev.code;
        }
    }
    return -1;
}

int main(void) {
    struct fb_ctx fb;
    if (fb_init(&fb) < 0) {
        fprintf(stderr, "fb init failed\n");
        return 1;
    }

    int input_fd = open("/dev/input/event0", O_RDONLY);

    int selected = 0;
    const char *items[] = {
        "Reboot system",
        "Wipe data",
        "Apply update",
        "Power off",
    };
    int n_items = sizeof(items)/sizeof(items[0]);

    while (1) {
        // 简化:仅用纯色背景表示当前选项
        uint32_t colors[] = {0xFF008000, 0xFF800000, 0xFF000080, 0xFF808000};
        fb_fill(&fb, colors[selected]);

        int key = read_key(input_fd);
        if (key == KEY_VOLUMEUP)    selected = (selected - 1 + n_items) % n_items;
        if (key == KEY_VOLUMEDOWN)  selected = (selected + 1) % n_items;
        if (key == KEY_POWER) {
            printf("Selected: %s\n", items[selected]);
            if (selected == 0) execl("/sbin/reboot", "reboot", NULL);
            if (selected == 3) execl("/sbin/poweroff", "poweroff", NULL);
            break;
        }
    }

    return 0;
}

这个程序展示了 Recovery UI 的底层本质:直接 mmap framebuffer + 读 input event,没有任何 GUI 库。


十、踩坑经验

  1. fstab 字段错一个,挂载失败 :Recovery 没法挂数据分区,wipe 不了 → 注意 wait,formattable 这些 flag
  2. AVB 拒绝刷未签名的 recovery:解锁后才能刷自制版本,或者要用 OEM 签名
  3. sideload(adb sideload)的工作目录 :推过来的包默认存到 /tmp/update.zip,大包会 OOM,要把 /tmp 挂大点
  4. fastbootd 和 recovery 共享 ramdisk :同一个二进制,只是启动参数不同(--fastboot)
  5. 国行机型可能锁 AVB key:无法刷自制 Recovery,需要单独的解锁工具

十一、总结

Recovery 是一个被严重低估的系统:

  • 它是一个完整的 Linux 系统,只是为了"救命"而设计得极度精简
  • BCB + /cache/command 是主系统与 Recovery 之间的桥梁
  • OTA 安装 + update-binary 是它的核心使命
  • 直接操作 framebuffer 让它在最恶劣的环境下也能工作

理解 Recovery 不仅能帮你做 OEM 定制,也能帮你在用户报"开不了机"时,快速定位 BCB 或者 fstab 的问题。最后一篇我们来讲 Bootloader 的世界 --- U-Boot 的移植实战。

相关推荐
应用市场4 小时前
eMMC 与 UFS 存储原理及在 Android 中的应用
android
随遇丿而安4 小时前
第4周:ImageView 最怕的不是不会显示图片,而是显示得“不对劲”
android
Mart!nHu4 小时前
Android10 添加以太网网络共享功能
android·以太网共享
修炼者6 小时前
bitmap和drawable的互相转换
android
美狐美颜SDK开放平台7 小时前
美颜SDK接入流程详解:Android、iOS、鸿蒙兼容方案解析
android·人工智能·ios·华为·harmonyos·美颜sdk·视频美颜sdk
笔夏8 小时前
【安卓学习之FloatingActionButton】按钮太小
android·学习
XD7429716369 小时前
科技早报晚报|2026年5月15日:无摄像头空间感知、Android 设备实验室与视频检索代理,今天更值得跟进的 3 个技术机会
android·科技·音视频·开源项目·边缘ai·开发者工具
应用市场9 小时前
Android Verified Boot 2.0 安全启动原理详解
android·安全
只可远观9 小时前
Android XML命令式和Jetpack Compose声明式UI
android·xml