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 库。
十、踩坑经验
- fstab 字段错一个,挂载失败 :Recovery 没法挂数据分区,wipe 不了 → 注意
wait,formattable这些 flag - AVB 拒绝刷未签名的 recovery:解锁后才能刷自制版本,或者要用 OEM 签名
- sideload(adb sideload)的工作目录 :推过来的包默认存到
/tmp/update.zip,大包会 OOM,要把/tmp挂大点 - fastbootd 和 recovery 共享 ramdisk :同一个二进制,只是启动参数不同(
--fastboot) - 国行机型可能锁 AVB key:无法刷自制 Recovery,需要单独的解锁工具
十一、总结
Recovery 是一个被严重低估的系统:
- 它是一个完整的 Linux 系统,只是为了"救命"而设计得极度精简
- BCB + /cache/command 是主系统与 Recovery 之间的桥梁
- OTA 安装 + update-binary 是它的核心使命
- 直接操作 framebuffer 让它在最恶劣的环境下也能工作
理解 Recovery 不仅能帮你做 OEM 定制,也能帮你在用户报"开不了机"时,快速定位 BCB 或者 fstab 的问题。最后一篇我们来讲 Bootloader 的世界 --- U-Boot 的移植实战。