Android Recovery 模式工作原理与定制实战
Recovery 是 Android 的"救命系统",负责 OTA 升级、恢复出厂、用户数据加密管理。本文剖析 Recovery 的架构、启动流程、与主系统的通信机制,并演示如何修改并构建一个自定义 Recovery。
一、Recovery 到底是什么?
很多人以为 Recovery 是 Android 系统的一个"模式",其实它是一个完全独立的 Linux 系统:有自己的 Kernel(实际上和主系统共享一个 boot 镜像的内核)、自己的 ramdisk、自己的 init、自己的应用。
它和主系统的关系类似:
PC 主系统 <-> PE 启动盘 Android <-> RecoveryRecovery 启动时只挂载最小化的文件系统,不启动 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:
intmain(intargc,char**argv){// 1. 重定向 stdio 到 last_logredirect_stdio("/tmp/recovery.log");// 2. 启动 logd 服务staticconstexprstructoptionOPTIONS[]={{"locale",required_argument,NULL,0},{"fastboot",no_argument,NULL,0},{0,0,0,0},};// 3. 加载 fstabautofstab=LoadFstab();// 4. 解析启动参数(来自 BCB)std::vector<std::string>args=get_args(argc,argv);// 5. 初始化 Device 抽象autodevice=make_device();autoui=device->GetUI();ui->Init(locale);// 6. 进入主循环InstallResult status=INSTALL_NONE;if(update_package!=nullptr){// OTA 包安装status=install_package(update_package,...);}elseif(should_wipe_data){wipe_data(device);}elseif(should_wipe_cache){wipe_cache(device);}elseif(should_wipe_ab){wipe_ab_device();}else{// 进入交互式菜单status=prompt_and_wait(device,retry_count);}// 7. 写回结果到 BCBfinish_recovery();// 8. 重启reboot("normal");return0;}prompt_and_wait 交互菜单
InstallResultprompt_and_wait(Device*device,intretry_count){staticconstchar*HEADERS[]={"Android Recovery","Use volume up/down to move highlight;","power button to select.",nullptr,};staticconstchar*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){intchosen=get_menu_selection(HEADERS,ITEMS,...);switch(chosen){case0:returnINSTALL_SUCCESS;case1:apply_from_adb(device);break;case2:apply_from_sdcard(device);break;case3:wipe_data(device);break;case4:wipe_cache(device);break;case5:mount("/system");break;case6:choose_recovery_file(device);break;case7:reboot("poweroff");break;}}}五、与主系统的通信:BCB(Bootloader Control Block)
我们在前文讲过,misc 分区存放 BCB。Recovery 主要靠 BCB 与外界沟通:
主系统触发 Recovery
// frameworks/base/services/core/java/com/android/server/RecoverySystem.javapublicstaticvoidrebootWipeUserData(Context context,...){bootCommand(context,"--wipe_data","--reason="+reason);}privatestaticvoidbootCommand(Context context,String...args){File RECOVERY_DIR=newFile("/cache/recovery");File COMMAND_FILE=newFile(RECOVERY_DIR,"command");StringBuilder command=newStringBuilder();for(String arg:args){command.append(arg).append("\n");}Files.write(command.toString().getBytes(),COMMAND_FILE);// 触发 reboot 到 recoverySystemProperties.set("sys.powerctl","reboot,recovery");}Recovery 启动后读命令
std::vector<std::string>get_args(intargc,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());}}returnargs;}退出时清理 BCB
voidfinish_recovery(){// 把 BCB 清掉,下次启动直接进主系统bootloader_message boot={};write_bootloader_message(&boot);// 删除 /cache 中的命令文件unlink("/cache/recovery/command");}六、OTA 安装的核心:install_package
Recovery 最重要的工作就是安装 OTA 包。流程:
InstallResultinstall_package(conststd::string&path,...){// 1. 校验 ZIP 签名 (PKCS#7)if(!verify_package(path,key_path)){returnINSTALL_CORRUPT;}// 2. 打开 ZIPZipArchiveHandle zip;if(OpenArchive(path.c_str(),&zip)!=0){returnINSTALL_CORRUPT;}// 3. 提取并执行 META-INF/com/google/android/update-binaryintpipefd[2];pipe(pipefd);pid_t pid=fork();if(pid==0){// 子进程close(pipefd[0]);charstatus_fd[8];snprintf(status_fd,sizeof(status_fd),"%d",pipefd[1]);execl("/tmp/update-binary","update-binary","3",// versionstatus_fd,path.c_str(),nullptr);exit(1);}// 父进程读 update-binary 的进度close(pipefd[1]);FILE*f=fdopen(pipefd[0],"r");charbuf[256];while(fgets(buf,sizeof(buf),f)){// 解析 "ui_print xxx", "progress xxx", "set_progress xxx"handle_progress(buf);}intstatus;waitpid(pid,&status,0);returnWEXITSTATUS(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
// bootable/recovery/recovery_ui/include/recovery_ui/ui.hclassRecoveryUI{public:virtualvoidInit(conststd::string&locale)=0;virtualvoidSetBackground(Icon icon)=0;virtualvoidSetProgress(floatfraction)=0;virtualvoidPrint(constchar*fmt,...)=0;virtualintWaitKey()=0;virtualvoidShowText(boolvisible)=0;};具体实现ScreenRecoveryUI直接操作 framebuffer:
voidScreenRecoveryUI::draw_screen_locked(){if(!show_text){// 显示进度条 + logodraw_background_locked();draw_foreground_locked();}else{// 显示文字菜单SetColor(MENU);// ...for(inti=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);}}}voidScreenRecoveryUI::update_screen_locked(){draw_screen_locked();gr_flip();// 把 framebuffer 内容显示到屏幕}gr_flip()内部通过 ioctl 通知 DRM 切换显示缓冲区:
intgr_flip(void){gr_draw=gr_backup_disp_buffer(gr_draw);structfb_var_screeninfovi;ioctl(fd,FBIOGET_VSCREENINFO,&vi);vi.yoffset=(current_buffer?vi.yres:0);ioctl(fd,FBIOPAN_DISPLAY,&vi);current_buffer=!current_buffer;return0;}八、定制 Recovery 实战
如果想给设备做一个自定义 Recovery,通常要做几件事:
1. 准备源码
# 拉 AOSP 源码repo init-uhttps://android.googlesource.com/platform/manifest-bandroid-13.0.0_r80 reposync# 配置自己的设备sourcebuild/envsetup.sh lunch<your_device>-userdebug2. 修改设备 Device 类
// device/yourcompany/yourdevice/recovery/recovery_ui.cpp#include"recovery_ui/device.h"#include"recovery_ui/screen_ui.h"classYourDevice:publicDevice{public:YourDevice(RecoveryUI*ui):Device(ui){}boolPostWipeData()override{// 擦数据后做点啥ResetCustomerSettings();returntrue;}BuiltinActionInvokeMenuItem(size_t menu_position)override{// 添加自定义菜单项if(menu_position==YOUR_CUSTOM_ITEM){DoSomethingCool();returnNO_ACTION;}returnDevice::InvokeMenuItem(menu_position);}};Device*make_device(){returnnewYourDevice(newScreenRecoveryUI);}3. 修改 BoardConfig.mk
# 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. 编译
makebootimage -j$(nproc)# 或者只编译 recovery (传统 boot/recovery 分离的设备)makerecoveryimage -j$(nproc)输出在out/target/product/<device>/:
boot.img— A/B 设备recovery.img— 传统设备
5. 刷写
adbrebootbootloader# A/B 设备fastboot flash boot boot.img# 传统设备fastboot flash recovery recovery.img九、minimal recovery UI 代码示例(脱离 AOSP)
下面是一个最简化的 framebuffer Recovery UI 主循环,展示原理:
#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>structfb_ctx{intfd;uint8_t*pixels;intwidth,height,stride;};staticintfb_init(structfb_ctx*fb){fb->fd=open("/dev/graphics/fb0",O_RDWR);if(fb->fd<0)return-1;structfb_var_screeninfovi;structfb_fix_screeninfofi;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_tsize=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;}staticvoidfb_fill(structfb_ctx*fb,uint32_tcolor){uint32_t*p=(uint32_t*)fb->pixels;intcount=fb->stride*fb->height/4;for(inti=0;i<count;i++)p[i]=color;}staticintread_key(intinput_fd){structinput_eventev;while(read(input_fd,&ev,sizeof(ev))==sizeof(ev)){if(ev.type==EV_KEY&&ev.value==1){// key downreturnev.code;}}return-1;}intmain(void){structfb_ctxfb;if(fb_init(&fb)<0){fprintf(stderr,"fb init failed\n");return1;}intinput_fd=open("/dev/input/event0",O_RDONLY);intselected=0;constchar*items[]={"Reboot system","Wipe data","Apply update","Power off",};intn_items=sizeof(items)/sizeof(items[0]);while(1){// 简化:仅用纯色背景表示当前选项uint32_tcolors[]={0xFF008000,0xFF800000,0xFF000080,0xFF808000};fb_fill(&fb,colors[selected]);intkey=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;}}return0;}这个程序展示了 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 的移植实战。
