☰
Android 5.1 Recovery升级流程全解析:从触发到脚本执行
2026/10/13 10:14:10 网站建设 项目流程

搞安卓系统的人,迟早都要跟 Recovery 打交道。特别是做 OTA 升级、固件维护、或者板级 Bringup 的时候,Recovery 就是那条“最后的安全通道”。Android 5.1 这个版本很有意思,它处在一个承上启下的位置——既有早期系统那种“裸奔”式的分区升级逻辑,又已经把升级脚本、签名校验、Cache 机制做得比较完整了。我当年第一次完整梳理它这套流程,是在一块用 RK3288 方案的板子上,通过一个假升级包一点点调试才弄明白全貌的。这篇文章就把我梳理过的 Android 5.1 Recovery 升级流程做个完整笔记,从触发方式、核心机制到脚本执行细节一步步拆解给有需要的朋友看。

1. 从按下“系统更新”到重启,系统都做了什么

在 Android 5.1 上,如果你在设置里点了“系统更新”,系统并不会直接在当前运行的状态下去改写分区。原因很简单:运行中的系统,它的根文件系统、system 分区、内核等等都是处于“正在使用”状态,你就算有权限去写这些分区,也很难保证写入过程中不会被正在跑的进程干扰,更没法保证写完以后文件系统的一致性。所以 Android 的经典升级思路就是——“先标记,再重启,进一个专门的系统去做升级操作”。

这个“专门的系统”,就是 Recovery。整个升级链路可以理解成三步走:触发方先把升级意图和参数写到一个约定好的位置;然后系统重启,引导层读到这个标记,加载 Recovery 内核和 ramdisk;Recovery 起来以后再根据这个标记去找到升级包、解析脚本、写入分区,完成后继续重启回到主系统。

Android 5.1 的系统更新触发,按来源可以分成几种典型路径:

  • 设置里的“系统更新”入口,调用系统升级服务发起。
  • 厂商自己写的 OTA 应用,通常通过一个叫做 RecoverySystem 的系统 API 来触发。
  • 工程模式下,通过 adb 命令直接往 /cache/recovery/ 或者 misc 分区写入升级标记。
  • 按键组合强行进入 Recovery,然后从 sdcard 或者 USB 设备读取升级包。

不管走哪条路径,最终的关键工作其实就两件:把升级标记写对地方,以及把升级包放在 Recovery 能读到的位置。这两点搞明白了,升级流程的大框架就立住了。

RecoverySystem 是系统侧的核心 API。它做的事情可以拆开看:

  1. 校验升级包是否存在、可读。
  2. 生成一个包含升级命令和升级包路径的字符串,例如--update_package=/data/media/0/update.zip。
  3. 把这个字符串写入/cache/recovery/command文件(有的实现直接写 misc 分区)。
  4. 调用Reboot重启到 Recovery 模式(reboot recovery)。

这里有一个容易被忽略的细节:写 command 文件需要 /cache 分区可写,如果系统因为异常导致 /cache 没有正常挂载,这一步可能失败。这也是为什么很多 OTA 失败案例里会看到 “Can't open /cache/recovery/command” 类似报错的原因。

另外,升级包的存放位置也很讲究。Android 5.1 时代,/data 分区往往是加密的,如果 Recovery 不解密 /data,它就读不到放在内部存储里的升级包。所以很多方案会把升级包放到 /cache 分区(Cache 分区默认不加密),或者让 Recovery 支持解密 /data 后再读取。这也是为什么工程师调试时经常看到升级包被拷贝了一份到 /cache 下的原因——那是系统侧为了保证 Recovery 能读到包所做的兼容处理。

到了这里,整个升级决策阶段的核心逻辑就清楚了:普通系统负责“下订单”,把升级指令写进“订单簿”,Recovery 负责“执行订单”。两个系统之间靠一个共享的标记区域通信,这个设计虽然古老,但非常可靠。

2. 升级标记与通信机制:command 文件与 BCB 细节

前面说的“订单簿”在 Android 5.1 里其实是两套机制:一套是/cache/recovery/command文件体系,另一套是 Bootloader 与 Recovery 共用的 misc 分区 BCB(Bootloader Control Block)。这两套不是互斥的,它们各自负责不同层面的引导决策。

/cache/recovery/command是 Recovery 自己的命令文件,它支持的命令行参数很丰富,我这里列几个实际常用的:

参数作用备注
--update_package=路径指定升级包路径最常见,OTA 升级的主命令
--wipe_data清除用户数据恢复出厂设置时用
--wipe_cache清除缓存分区常与 wipe_data 搭配
--show_text显示日志文本调试时用
--locale=zh_CN设置 Recovery UI 语言影响界面文案
--just_exit直接退出 Recovery用于重启逻辑控制
--shutdown_after升级完成后关机某些场景需要

Recovery 启动时会优先去读 /cache/recovery/command 文件,一行一行解析参数,把解析结果存到一个全局结构体里。这个结构体会指导后续整个升级流程的行为。如果 command 文件不存在或者为空,Recovery 就会进入交互模式——也就是用户看到的那个带菜单的 Recovery 界面。

再说 BCB。misc 分区是一块小的、专用的分区,Bootloader 和 Recovery 都会去读写它。它的结构里包含一个bootloader_message结构体,里面有关键的三个字段:

struct bootloader_message { char command[32]; char status[32]; char recovery[768]; };

command字段通常用来放类似boot-recovery这样的引导指令,recovery字段则是留给 Recovery 的命令参数。比如recovery字段的内容可能长这样:

--update_package=/data/media/0/update.zip --wipe_cache

Bootloader 在启动时会检查 misc 分区的 BCB:如果command字段被设置为boot-recovery,那就说明系统希望进入 Recovery 模式。这时候 Bootloader 就会去加载 Recovery 的内核和 ramdisk,而不是正常 Boot 内核。

听起来有点绕,但梳理清楚了就不乱。系统的引导决策链是这样的:

  1. 系统侧写入 command 文件和/或 BCB。
  2. 设备重启。
  3. Bootloader 首先看 BCB 的 command 字段,决定引导哪个镜像。
  4. 如果引导了 Recovery 镜像,Recovery 进程启动后,第一件事儿就是去读 /cache/recovery/command,拿取具体的升级指令。
  5. BCB 中的status字段则用来记录升级状态,例如升级成功、失败等,Bootloader 和 Recovery 都可以读取它来做后续判断。

这个机制有一个很经典的好处:即使系统已经无法正常启动,只要 Bootloader 还能跑,用户依然可以通过按键组合让设备进入 Recovery 模式,然后手动选择升级包恢复系统。这也是各家刷机模式(某些厂商所谓的“工程模式”)的底层逻辑——本质上就是让人能手动触发 Recovery 的交互流程。

我在实际调试中看到过一个问题:Recovery 升级完成后,设备重启,结果又反复进入 Recovery 模式。排查到最后发现是 BCB 没有被正确清除导致的。Recovery 在完成所有任务后,应该调用clear_bootloader_message()之类的函数清空 BCB 和 command 文件。如果这一步失败了,Bootloader 下一次启动时依然会看到boot-recovery标记,自然就再次进了 Recovery。遇到这种"鬼打墙"一样的重启问题,优先查这个东西,比瞎改代码高效得多。

3. Recovery 启动后的初始化与升级包校验

Recovery 内核起来之后,进到用户态,第一个进程就是 init,它会按照 ramdisk 里的 init.rc 启动 Recovery 主程序。Android 5.1 里对应的路径在bootable/recovery/下,入口是recovery.cpp。

主程序初始化做的事情可以按顺序列出来:

  1. 设置 updataer 进程需要的环境变量(例如 PATH 等)。
  2. 初始化日志系统,后续所有输出都会写入/tmp/recovery.log。
  3. 读取 /cache/recovery/command 解析命令参数。
  4. 初始化屏幕、按键、语言资源。
  5. 根据解析得到的命令,走对应的分支。有升级任务就走升级流程,没有就进入菜单交互。

Recovery 的文件系统是一个精简的 ramdisk 环境,里面只有一个最小的 Linux 环境,包含必要的工具如 mount、sh、toybox 等。它跟正常系统的区别在于:Recovery 环境不会去挂载 /system 和 /data 的可读写文件系统,除非升级脚本里显式要求挂载它们。这个设计是对的——你就是要准备往这些分区写新内容,如果一上来就把它们挂成可写,那后续操作反而容易出问题。

升级包校验这个环节值得展开说。Android 5.1 对升级包有严格的签名校验机制,核心目的是防止篡改过的升级包被刷入设备。整个校验链路的逻辑是这样:

  • 升级包必须是 ZIP 格式,签名信息放在 META-INF 目录下。
  • 校验分为“整个包签名验证”和“单个文件验证”两层。整个包签名用的是 v1 签名方案(JAR 签名);Recovery 在拿到升级包后会先做一次全包签名验证,验证通过才会解析执行升级脚本。
  • 整个包校验用的公钥是编译 Recovery 时内置进来的,位于源码的/bootable/recovery/keys/目录下。厂商如果换了签名的 key,必须同步替换这个内置公钥,否则 Recovery 会直接拒绝这个包。
  • 升级脚本里的每条写入指令,比如写 boot、写 system 下的文件,底层都会再次校验针对单个文件的哈希和签名,防止包被拆分重组。

这一步对实际调试的影响非常直接。我见过太多开发板子上的“升级失败”最终原因都是签名不一致:电脑上拿测试 key 签了一个包,板子的 Recovery 里内置的却是正式 key,结果一刷就报signature verification failed。有些方案为了调试方便会去掉签名校验,但这不是个值得提倡的做法——如果连签名校验都关了,升级通道就是裸奔的。

校验通过之后,Recovery 会解压出升级脚本来解析执行。这里有一个大家容易搞混的概念:升级脚本不是 shell 脚本,而是用 edify 语言编写的描述文件。它的作用更像一份“操作清单”,描述了这个包要往哪些分区写入哪些文件、要执行哪些操作。edify 脚本存放在META-INF/com/google/android/updater-script。

如果打开了升级包的 ZIP,你会看到典型的结构大致是这样:

update.zip ├── boot.img ├── system/ │ ├── build.prop │ ├── app/ │ └── framework/ ├── META-INF/ │ ├── com/ │ │ ├── google/ │ │ │ └── android/ │ │ │ ├── update-binary │ │ │ ├── updater-script │ │ │ └── otacert │ │ └── android/ │ │ ├── update-binary │ │ └── updater-script │ └── MANIFEST.MF └── ...

update-binary是一个可执行文件(实际是一个二进制程序或者脚本),它负责解析updater-script并执行其中描述的操作。整个系统在 Android 5.1 时代的设计就是:Recovery 进程把控制权交给 update-binary,update-binary 逐条读取、执行脚本里的命令,然后把执行状态返回给 Recovery 进程。

这里还有一个容易踩的坑:update-binary是随升级包一起分发的,本质上它跟 Recovery 不是一个“出生”的。如果厂商对 Recovery 做了深度定制,而升级包里的 update-binary 又比较旧,可能就会出现脚本解析异常、升级中断这类问题。遇到这种情况,优先检查升级包里的 update-binary 版本是否配套,而不是一头扎进脚本逻辑里找问题。

4. 升级流程的“发动机”:升级脚本与二进制执行器

Recovery 升级流程里真正干活的,是 update-binary 与 updater-script 的配合。edify 语言本身不复杂,但它定义了一整套用于“分区读写、文件解压、系统配置”的 DSL(领域专用语言)。我在 5.1 上实际调试时,最常用的就是下面这一小撮函数:

函数名作用使用场景
mount(fs_type, partition_type, location, mount_point)挂载分区通常先挂载 /system
package_extract_file("path", dest)从包内解压文件到目标位置推送单个系统文件或镜像
package_extract_dir("dir", dest)从包内解压目录到目标位置整目录更新 /system 下的内容
ui_print("...")在屏幕显示进度/提示信息显示升级步骤提示
set_progress(progress)设置升级进度条配合 ui_print 给用户反馈
delete(path)删除文件或目录清理废弃的系统文件
write_raw_image(package_path, partition)写入原始镜像到分区刷 boot/recovery
run_program(path, args...)执行外部程序需要调用工具时的兜底方案
format(fs_type, partition_type, location, fs_options)格式化分区部分包需要先格式化某些分区
assert(condition)条件检查,失败即中止版本号、型号匹配检查

一个典型的系统 OTA 升级脚本,开头通常先做断言检查,再挂载分区,然后依次解压文件、删除废弃文件、刷镜像,最后卸载分区。我见过不少新人写脚本时忽略顺序问题,比如还没挂载 /system 就开始package_extract_dir,结果当然是失败。edify 本身不会帮你自动挂载分区,所有操作都是显式的。

写一个 PATCH 性质的升级脚本(这里指的是对现有系统做增量修改的场景)时,需要注意一些 5.1 时代特有的事:

  • package_extract_dir只能把文件解压到已经挂载的分区上,不能跨越分区挂载点写入,否则路径会解析到临时目录里然后失败。
  • delete只能删除已挂载分区上的文件,删除前要先确保分区挂载无误。
  • 如果升级涉及 /system 下的.so文件替换,脚本里通常会先记录原文件的 SELinux 上下文,升级完再恢复。Android 5.1 里 SELinux 策略相对复杂,文件的安全上下文如果错了,重启后服务起不来,表现就是一堆avc: denied的 log,排查起来很痛苦。

再来说 update-binary 的执行细节。Recovery 会 fork 一个子进程去运行update-binary,然后用管道读取它的输出。ui_print的输出、进度进度百分比、错误信息等,都是通过这个管道从 update-binary 传回 Recovery 的。Recovery 再把这些信息显示到屏幕上,并且写入/tmp/recovery.log。

update-binary 执行结束后的退出码也是一个关键信号。约定俗成的规则是:0表示成功,非零表示失败。Recovery 拿到退出码后,会决定后续动作:成功就继续清理、重置 BCB、重启;失败就中止升级,保留现场(包括日志),并在屏幕上显示错误代码。

Android 5.1 的错误码设计其实比较粗糙,但和升级流程密切相关。最常见的几个需要记一下:

错误码含义常见诱因
0成功—
1升级包使用错误脚本assert失败、包内容不匹配
2升级包损坏或校验失败包下载不完整、签名错误
3升级包不受支持平台/Variant 匹配不上
4存储空间不足/cache 或目标分区空间不够
6分区写入失败分区表异常、Flash 坏块
7升级包与设备不匹配版本号、硬件型号断言没通过

这些错误码是现场排查的第一把钥匙。看到status 7基本不用多想,先去查升级脚本开头的assert检查了什么,是不是版本校验卡住了。看到status 2,优先查包完整性、签名,而不是去猜分区问题。

5. 整个升级过程的实际操作链路(从标记到重启的完整时序)

前面把机制讲得差不多了,这里把整个 Android 5.1 “从标记到重启”的完整时序串起来,按时间线展开。我梳理的这条链路,是手上那块 RK3288 板子实测走通的路径,绝大部分 Android 5.1 方案都大同小异。

第一步,系统侧发起升级。OTA 应用调用 RecoverySystem API,把包含升级包路径的命令写入/cache/recovery/command,同时写入 BCB,设置command=boot-recovery,recovery=--update_package=...,然后调用系统属性触发重启。重启命令通常走reboot recovery。

第二步,Bootloader 读 BCB。这部分是芯片厂商实现的。它会判断command是否为boot-recovery,是则加载 Recovery 的内核和 ramdisk,否则正常引导。这一阶段如果 misc 分区读写异常或者分区表配置错误,就会出现“重启后无法进入 Recovery 又无法正常开机的双输局面”,基本只能靠工具刷机救回来。

第三步,Recovery 内核启动并进入用户态。init 执行后启动 recovery 主程序,recovery 主程序解析 command 文件。解析完确认有升级任务后,会先做一些环境准备:比如初始化日志、挂载 /tmp 等。

第四步,校验升级包。Recovery 对升级包做签名验证。验证通过后进入下一步。

第五步,fork 出 update-binary 子进程。升级包的 updater-script 被逐条执行。这个过程里,分区的挂载、文件写入、镜像刷入都发生在这一阶段。屏幕上的进度条也是此时在跳动。

第六步,升级完成后清理现场。update-binary 退出并返回 0,Recovery 收到成功状态后,会做几件事:清除 BCB 和 command 文件、删除 /cache 下无用的临时文件(比如升级包拷贝)、保留 /tmp/recovery.log 的快照以便后续分析。

第七步,重启。Recovery 执行 reboot。Bootloader 再读 BCB,发现 command 已被清空,正常启动主系统。

这里还要多提一句:Android 5.1 的 Recovery 在升级完成后,默认会尝试把升级日志共享出来,方便工程师排查。具体路径通常在做完清理前拷贝到/cache/recovery/last_log或者/data/misc/log/recovery.log。如果你升级失败了,第一件事不是翻代码,而是把这个 log 拉出来读。

整体时序其实不复杂,复杂度全藏在细节里。比如升级脚本里package_extract_file写 boot 分区的时候,文件会先解压到内存临时目录,再通过write_raw_image写入分区,如果镜像文件太大、内存又紧张,可能在这一步挂掉。再比如某些文件系统在写入大量小文件时性能极差,解压几百个 framework 文件要几分钟,用户看着进度条卡死就会误以为升级失败。这些都是实际操作中才会遇见的场景,光看代码是想不全的。

6. 三个典型问题与排查路径

最后分享几个我在 Android 5.1 升级流程调试中真实踩过的坑,以及对应的排查路径,希望能帮大家少走弯路。

第一个案例,升级到一半提示错误,log 里看到E:Error in /tmp/sideload/package.zip (Status 7)。Status 7 是 assert 检查失败,这个检查通常是升级脚本开头的getprop("ro.product.device")或者ro.build.version.release之类的断言跟当前系统不匹配。排查时,先看升级包updater-script开头的 assert 语句具体要求了哪些属性,然后用 adb shell getprop 对比当前系统的属性值。多数情况是包的 target 版本跟当前系统版本不一致,或者包的 device 字段跟板子实际型号对不上。

第二个案例,升级包签名验证失败,log 里出现E:failed to verify whole-file signature。遇到这个,先别急着怀疑包坏了。优先检查 Recovery 内置公钥和签名用的私钥是否配对。很多项目开发阶段用 debug key,量产切了 release key,结果 Recovery 也是不同阶段编译的,公钥自然就配不上。还有一个隐蔽场景:OTA 包制作工具和签名工具版本不一致,导致签出来的包结构不符合 v1 签名规范,也会出现这个错误。

第三个案例,E:Can't open /cache/recovery/command。这个错误多半是 /cache 分区没有成功挂载导致。5.1 时代常见原因包括:分区表损坏、/cache 被上次异常升级格式化成了不支持的文件系统、或者系统侧写入 command 文件时 /cache 已经满了。排查思路是先确认 /cache 能否正常挂载,再确认文件系统类型,然后检查空间余量。如果设备已经升级失败变砖(卡在 Recovery 循环),可以先通过按键进手动模式,用工具格式化 /cache 再试。

以上三个案例的共性经验是:所有的排查都要以 log 为准,不要猜。Recovery 的日志虽然看着简陋,但里面的时间线、错误码、路径信息足够还原绝大多数问题的现场了。我在拿到一块出问题的板子时,第一件事永远是adb pull /tmp/recovery.log,而不是急着重刷包。

Android 5.1 的 Recovery 升级流程,放到今天看依然是理解安卓升级机制的绝佳样板。它没有后来 A/B 分区、动态分区那么复杂的抽象层,每个环节都直来直去,但正因为直白,反而特别适合拿来建立对“分区、标记、脚本、签名”这套体系的心智模型。真搞懂这套流程之后,再去看后来的无缝升级、VAB 分区,会觉得很多设计都是一脉相承的。希望这篇笔记能帮你把这条链路彻底理顺。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询