1. 从一次OTA升级失败说起:Recovery到底在干什么
很多人第一次接触Recovery,都是因为设备OTA升级失败卡在开机界面,或者手动进入恢复模式后不知道那些菜单项背后发生了什么。我最早接触这块是在一个定制设备项目上,当时系统推送了一个差分包,设备重启后直接黑屏,log也抓不到,最后只能拆机短接进恢复模式重新刷全量包。那次之后我才认真把Recovery的启动链路和OTA安装流程从头到尾捋了一遍。
Recovery本质上是Android系统里一个独立的最小化运行环境,它和正常启动的Android系统共用内核,但挂载的是另一个根文件系统,运行的是一个精简的二进制程序。它的核心职责就三件事:恢复出厂设置、应用OTA更新包、执行一些维护性操作。OTA安装是其中最关键也最容易出问题的一环,因为它涉及分区挂载、签名校验、差分包合并、写入bootloader等一连串高风险动作,任何一步出错都可能导致设备变砖。
这篇文章适合谁看?如果你在做Android系统定制、OTA推送平台开发、设备固件维护,或者单纯想搞清楚设备重启后那几十秒里到底发生了什么,那这篇内容应该能帮到你。我会从Recovery的启动入口讲起,把OTA安装的架构拆开,重点说清楚差分包和全量包的处理差异、签名校验的完整链路、以及实际调试中那些文档里不会写的坑。全文基于AOSP的通用实现逻辑,不同厂商的定制会有差异,但主干流程是一致的。
2. Recovery的启动入口与运行环境搭建
2.1 从bootloader到Recovery内核的跳转逻辑
设备重启进入Recovery,第一步是bootloader根据misc分区的命令决定启动哪个系统。misc分区里存着一个bootloader_message结构体,里面有个command字段,当这个字段被写入"boot-recovery"时,bootloader就会加载recovery分区里的内核和ramdisk,而不是正常的boot分区。这个机制是理解整个OTA流程的起点,因为OTA安装的本质就是:正常系统往misc分区写命令,然后重启,bootloader读到命令后引导进Recovery,Recovery再根据command字段里的具体指令去执行对应操作。
misc分区的结构在AOSP里定义得很清楚,command字段之后还有recovery字段,用来传递更具体的参数,比如"--update_package=/data/ota_package/update.zip"这种。Recovery启动后会解析这些参数,决定是执行OTA安装、清除数据还是其他操作。这里有个容易忽略的细节:misc分区的写入必须保证原子性,如果写到一半断电,command字段可能处于半写状态,bootloader读到一个非法值就可能卡住。所以正常系统在写misc之前会先擦除整个分区,再一次性写入完整结构,这个顺序不能反。
2.2 Recovery ramdisk里的关键组件
Recovery的ramdisk里包含了一个精简的init程序、recovery二进制本身、以及一些必要的库和配置文件。init会先挂载/sys、/proc、/dev这些基础文件系统,然后根据init.rc里的配置启动recovery服务。recovery二进制是整个流程的核心,它内部实现了一个简单的事件循环和UI框架,同时集成了OTA安装的逻辑。
recovery的UI框架很原始,就是基于framebuffer的直接绘制,没有SurfaceFlinger那一套。它支持触摸和按键两种输入,但不同设备的输入设备节点不一样,所以经常出现Recovery界面能显示但触摸没反应的情况。这通常是因为init.rc里没有正确配置触摸设备的权限,或者recovery的输入事件映射表里没有对应设备的键值。调试这类问题时,可以先把recovery的log级别调高,看它有没有识别到输入设备。
recovery二进制在启动时会先检查/cache/recovery/command文件,这个文件是正常系统在重启前写入的,内容和misc分区里的command字段一致。如果这个文件存在,recovery会优先读取它,这给调试带来了方便——你可以手动往这个文件里写命令,然后重启进Recovery来测试特定流程,不用每次都从正常系统触发。
2.3 分区挂载的先后顺序与依赖关系
Recovery启动过程中,分区挂载的顺序很讲究。system和vendor分区在Recovery模式下通常不挂载,因为OTA安装不需要它们。必须挂载的是cache分区(用来存log和命令文件)、data分区(如果OTA包放在data里)、以及misc分区(读写启动命令)。外置存储如sdcard的挂载则取决于具体实现,有些设备在Recovery下不挂载sdcard,导致放在sdcard上的OTA包无法访问。
挂载顺序上,一般是先挂载tmpfs到/tmp,再挂载cache到/cache,然后根据命令参数决定是否挂载data和sdcard。这里有个坑:如果cache分区损坏或者文件系统不一致,挂载会失败,recovery会卡在挂载等待上。我遇到过一台设备因为cache分区被写满导致挂载超时,最后是通过在recovery的init.rc里加了挂载超时和重试逻辑才解决。实际调试时可以用mount命令手动挂载看具体报错,但Recovery下的shell功能很有限,通常需要把busybox集成进去才方便操作。
3. OTA包的两副面孔:全量包与差分包的处理差异
3.1 全量包的结构与安装流程
全量OTA包就是一个完整的system/vendor/boot等分区的镜像集合,外面包了一层zip。zip里面有个META-INF/com/google/android/updater-script和updater-binary,前者是安装脚本,后者是执行引擎。Recovery在安装全量包时,会先校验zip的签名,然后解压出updater-binary并执行,updater-binary再解析updater-script里的命令,逐条执行分区写入操作。
全量包的安装相对简单粗暴,就是把镜像直接写到对应分区。但这里有个细节:写boot分区之前必须先校验镜像的完整性,否则写坏了boot分区设备就起不来了。AOSP的updater-binary里对boot分区的写入有额外的保护逻辑,会先检查镜像的magic number和大小是否匹配分区容量。实际项目中,如果自定义了分区布局,一定要同步修改updater-binary里的分区表,否则会出现写错分区或者写越界的情况。
全量包的签名校验用的是整个zip包的签名,Recovery会用内置的公钥去验证。这个公钥是在编译Recovery时通过PRODUCT_DEFAULT_DEV_CERTIFICATE指定的,如果OTA包的签名私钥和Recovery里的公钥不匹配,校验就会失败。很多团队在切换签名密钥时忘了同步更新Recovery,导致所有OTA包都装不上,这个坑很常见。
3.2 差分包的工作原理与合并过程
差分包不是完整镜像,而是新旧版本之间的二进制差异数据。它的体积通常只有全量包的10%到30%,对减少推送流量很有意义。差分包的核心是bsdiff算法生成的patch文件,Recovery在安装时需要用旧版本的镜像作为输入,应用patch后生成新版本镜像,再写入分区。
这个流程对旧版本镜像的完整性要求极高。如果设备上的旧镜像和生成差分包时用的旧镜像有任何字节差异,patch应用就会失败。实际中最常见的问题是设备上的system分区被修改过(比如用户刷了第三方模块),导致差分包校验失败。所以差分包安装前,Recovery会先校验旧镜像的哈希值,这个哈希值是在生成差分包时计算并写入updater-script的。校验不通过就直接报错退出,不会尝试写入。
差分包的合并过程很耗内存和CPU,因为bsdiff的patch应用需要把旧镜像和新镜像都加载到内存里。对于大分区(比如2GB的system),Recovery的可用内存可能不够,这时候就需要用流式处理或者分块合并。AOSP的updater-binary里对这种情况有优化,会把镜像分成若干块逐块处理,但块大小的选择会影响合并速度和内存占用。块太小速度慢,块太大内存不够,通常需要根据设备实际内存来调整。
3.3 两种包在Recovery里的分支判断
Recovery在安装OTA包时,会先读取zip里的metadata文件,判断这是全量包还是差分包。metadata里有个"ota-type"字段,值为"AB"或"BLOCK",还有"pre-device"字段用来校验设备型号。如果设备型号不匹配,安装会直接中止,这是防止刷错包的重要保护。
判断为差分包后,Recovery会检查updater-script里的"source_hash"字段,这个字段记录了旧镜像的哈希。Recovery会计算设备上对应分区的哈希并比对,一致才继续。这里有个容易忽略的点:哈希计算的范围是整个分区还是分区里的有效数据?AOSP的实现是计算整个分区的哈希,包括末尾的空闲区域。如果设备在OTA前做过分区调整(比如resize),空闲区域的内容会变化,导致哈希不匹配。所以差分包推送前一定要确保设备分区状态和生成差分包时的基准一致。
4. 签名校验的完整链路与常见绕过陷阱
4.1 从zip签名到镜像签名的多层校验
OTA安装过程中的签名校验不是单一层次的,而是从外到内有多层。最外层是zip包的签名,Recovery用公钥验证整个zip的完整性。第二层是updater-binary的签名,这个二进制在编译时就被签名,Recovery执行前会校验它的签名。第三层是各个分区镜像的签名,比如boot镜像的签名会在写入前校验。
这三层校验的密钥可以不同,但通常用同一套密钥。实际项目中,有些团队为了灵活会分开管理,但这样会增加密钥泄露的风险。我的建议是至少保证zip签名和updater-binary签名用同一套密钥,因为这两者是最容易被替换的攻击点。镜像签名可以单独管理,但也要确保Recovery里有对应的公钥。
校验失败时的错误信息很关键。Recovery会输出具体的失败原因,比如"signature verification failed"或者"package expects build fingerprint of..."。前者是签名不匹配,后者是设备型号或版本不匹配。调试时先看错误信息,再定位是密钥问题还是包本身的问题。
4.2 公钥的存放位置与替换方法
Recovery里的公钥通常存放在/res/keys或者/system/etc/security/otacerts.zip里。不同Android版本的存放位置有差异,Android 9之后倾向于放在/system/etc/security/otacerts.zip,而更早的版本可能直接放在ramdisk的/res/keys目录下。替换公钥需要重新编译Recovery,或者通过修改ramdisk里的文件来实现。
如果只是临时调试,可以修改Recovery的源码,把签名校验逻辑跳过。但生产环境绝对不能这么做,因为跳过签名校验意味着任何包都能被安装,设备安全性完全丧失。我见过一些团队为了调试方便在debug版本里跳过校验,结果debug版本流出到用户手里,造成了严重的安全问题。所以即使调试,也建议用测试密钥而不是完全跳过校验。
4.3 校验失败时的排查路径
签名校验失败时,第一步是确认Recovery里的公钥和OTA包的签名私钥是否匹配。可以用openssl命令提取OTA包的签名证书,和Recovery里的公钥做比对。如果公钥不匹配,需要重新签名OTA包或者更新Recovery里的公钥。
第二步是确认OTA包的签名算法。Android支持SHA256withRSA和SHA1withRSA等算法,如果Recovery编译时没有启用对应的算法支持,校验也会失败。这种情况在跨版本OTA时比较常见,比如用新工具签的包在老设备上装不上。
第三步是检查zip包本身是否完整。有时候下载过程中zip包损坏,签名校验也会失败。可以用unzip -t命令测试zip的完整性,或者重新下载OTA包再试。实际项目中,OTA推送平台应该对上传的包做完整性校验,避免损坏的包被推送到设备。
5. 分区写入的底层操作与断电保护机制
5.1 块设备写入的原子性与对齐要求
OTA安装最终要把数据写到块设备上,比如/dev/block/by-name/system。块设备的写入不是原子的,如果写到一半断电,分区就会处于不一致状态。为了防止这种情况,Recovery在写入关键分区(如boot、system)时会采用一些保护措施。
首先是写入前的擦除。对于NAND闪存,写入前必须先擦除对应的块,否则写入会失败。Recovery会调用ioctl的BLKDISCARD命令来擦除分区,这个操作比较耗时,但能保证后续写入的正确性。对于eMMC和UFS,擦除不是必须的,但为了性能考虑,通常也会先discard再写入。
其次是对齐。块设备的读写必须按扇区对齐,通常是512字节或4096字节。如果写入的数据长度不是扇区大小的整数倍,Recovery会在末尾补零。这个补零操作看起来无害,但如果分区末尾有重要的元数据(比如某些设备的boot分区末尾有签名信息),补零就会破坏它。所以自定义分区布局时,一定要确保镜像大小是扇区大小的整数倍。
5.2 写入过程中的断电恢复策略
断电是OTA安装最怕遇到的情况。如果正在写boot分区时断电,设备就彻底起不来了。为了降低风险,Recovery在写入boot分区前会先把旧boot镜像备份到cache分区,写入完成后再删除备份。如果写入过程中断电,下次启动时bootloader会发现boot分区损坏,自动进入Recovery,Recovery再从cache里恢复旧boot镜像。
这个备份恢复机制不是AOSP默认就有的,需要厂商自己实现。实现的关键是备份的完整性和恢复的可靠性。备份文件要带校验和,恢复前先校验备份文件的完整性。恢复过程中如果再次断电,要有重试机制。我见过一些设备的恢复逻辑写得不严谨,恢复过程中断电后设备就彻底变砖了,只能返厂。
对于system和vendor分区,断电的后果没那么严重,因为这两个分区不参与启动引导。但断电会导致分区数据不一致,下次OTA时差分包校验会失败。所以OTA安装完成后,Recovery会往misc分区写一个"boot-success"标记,正常系统启动后读到这个标记才认为OTA成功。如果没读到,系统会回滚到旧版本或者重新触发OTA。
5.3 写入完成后的校验与标记
写入完成后,Recovery会重新读取分区内容并计算哈希,和OTA包里记录的哈希比对。这个校验是最后一道防线,能发现写入过程中的静默错误。校验通过后,Recovery会更新misc分区的command字段,清除"boot-recovery"标记,并写入"boot-success"标记。
这里有个细节:misc分区的更新必须在所有分区写入完成并校验通过之后。如果提前更新了misc,但后续分区写入失败,设备重启后会进入正常系统,而正常系统读到的分区是旧的,就会出现版本不一致的问题。所以misc的更新是OTA安装的最后一步,也是整个流程的原子提交点。
6. 调试OTA安装的实战手段与log分析
6.1 Recovery log的抓取与关键字段解读
Recovery的log默认输出到/cache/recovery/last_log,如果cache分区挂载失败,log会输出到/tmp/recovery.log。抓log的方法有两种:一种是在Recovery界面里通过按键组合触发log保存到sdcard,另一种是通过adb连接(如果Recovery启用了adbd)。很多设备的Recovery默认不启用adbd,需要修改init.rc或者编译时打开。
log里的关键字段包括:Starting recovery表示Recovery启动成功,Command:后面跟的是从misc或command文件读到的命令,Verifying update package表示开始签名校验,Installing update表示开始安装,Write开头的行表示分区写入操作。如果安装失败,log里会有E:开头的错误行,比如E:Error in /sideload/package.zip。
分析log时要注意时间戳。如果某个操作卡了很久,说明那里可能有问题。比如签名校验卡住可能是公钥不匹配导致的重试,分区写入卡住可能是块设备有问题。我遇到过一次写入卡住是因为eMMC寿命耗尽,写入速度极慢,最后只能换主板。
6.2 手动构造command文件测试特定流程
调试OTA安装时,不用每次都从正常系统触发。可以直接在Recovery模式下往/cache/recovery/command文件里写命令,然后重启进Recovery。命令格式和misc分区里的command字段一致,比如:
--update_package=/sdcard/update.zip --wipe_cache这样Recovery启动后会直接执行OTA安装,省去了正常系统写misc和重启的步骤。这个方法在调试差分包时特别有用,因为可以快速替换OTA包重试,不用每次都刷回旧版本。
需要注意的是,手动构造command文件时,文件权限和换行符要正确。文件必须是root可读,每行一个参数,行尾用\n而不是\r\n。如果格式不对,Recovery会解析失败,log里会有Failed to parse command file的错误。
6.3 常见失败场景的快速定位表
| 失败现象 | 可能原因 | 排查方法 |
|---|---|---|
| 卡在开机logo | boot分区写入失败 | 检查boot镜像大小是否超过分区容量 |
| 进入Recovery后黑屏 | framebuffer初始化失败 | 检查内核的framebuffer驱动和分辨率配置 |
| 签名校验失败 | 公钥不匹配或包损坏 | 比对公钥,用unzip -t测试包完整性 |
| 差分包合并失败 | 旧镜像哈希不匹配 | 检查设备分区是否被修改过 |
| 写入过程中断电 | 电源不稳定或电池电量低 | 确保电量>50%再OTA,加断电恢复逻辑 |
| 安装完成后无法启动 | misc标记未正确更新 | 检查misc分区的command字段是否被清除 |
这个表是我在实际项目中总结的,覆盖了大部分常见问题。但每个设备的具体表现可能不同,关键是要养成看log的习惯。Recovery的log虽然简陋,但信息量足够定位大部分问题。
7. 从A/B分区到虚拟AB:OTA架构的演进与选型思考
7.1 传统A/B分区OTA的工作机制
A/B分区OTA是Android 7.0引入的机制,设备有两套完整的system/boot分区,一套在用(active),一套备用(inactive)。OTA时把新版本写到备用分区,写完后切换active标记,重启后从新分区启动。如果新分区启动失败,bootloader会自动回滚到旧分区。
这个机制的最大好处是OTA过程不影响设备使用,安装可以在后台进行,用户只需要最后重启一次。而且回滚是自动的,不需要Recovery参与。但代价是存储空间翻倍,对于存储紧张的设备不太友好。
A/B分区OTA的安装逻辑和传统Recovery OTA完全不同。它不需要进入Recovery,而是在正常系统里通过update_engine服务把数据写到备用分区。update_engine支持流式写入,边下载边写入,进一步减少了安装时间。但update_engine的差分包处理和Recovery的updater-binary不一样,它用的是自己的delta算法,兼容性需要额外验证。
7.2 虚拟A/B分区的空间优化与代价
虚拟A/B(Virtual A/B)是Android 11引入的,目的是在A/B分区的基础上减少存储占用。它只保留一套物理分区,但通过snapshot机制在OTA时创建快照,把新数据写到快照里。安装完成后,快照合并到物理分区,设备重启后从合并后的分区启动。
虚拟A/B的优点是存储占用小,缺点是合并过程复杂,对文件系统的要求高。它需要文件系统支持snapshot,目前主要是ext4和f2fs支持。合并过程中如果断电,快照可能损坏,需要Recovery介入修复。所以虚拟A/B设备通常还是保留了一个精简的Recovery,用于处理合并失败的情况。
选型时怎么选?如果设备存储充足,对OTA体验要求高,选传统A/B。如果存储紧张,能接受稍长的OTA时间,选虚拟A/B。如果设备很老,存储和内存都很有限,那还是用传统Recovery OTA。没有绝对的好坏,只有适不适合。
7.3 不同OTA架构对Recovery的依赖程度
传统Recovery OTA对Recovery的依赖最重,整个安装过程都在Recovery里完成。A/B分区OTA对Recovery的依赖最轻,只在回滚失败时才需要Recovery。虚拟A/B介于两者之间,合并失败时需要Recovery修复。
这个差异直接影响Recovery的裁剪策略。如果设备用A/B分区OTA,Recovery可以裁得很小,只保留基本的清除数据和修复功能。如果用传统Recovery OTA,Recovery必须包含完整的OTA安装逻辑,体积会大很多。实际项目中,Recovery分区的大小通常是boot分区的1.5到2倍,如果空间不够,就要考虑裁剪或者换OTA架构。
我在一个项目里遇到过Recovery分区太小导致OTA包放不下的问题。当时的解决方案是把OTA包放在data分区,Recovery从data分区读取。但这要求data分区在Recovery下可挂载,而且data分区不能加密。如果设备启用了文件级加密,Recovery下data分区是加密的,读不了OTA包。所以这个方案只适用于未加密设备,或者把OTA包放在单独的未加密分区里。
8. 实际项目中那些文档不会告诉你的经验
8.1 电量与温度对OTA成功率的影响
OTA安装过程中,设备会持续写入闪存,功耗比正常使用高。如果电量低于30%,安装过程中可能因为电量耗尽而断电。所以OTA前一定要检查电量,低于50%就提示用户充电。这个检查在正常系统里做,不通过就不写misc分区,不触发重启。
温度也是个大问题。闪存在高温下写入速度会下降,甚至出现写入错误。夏天在户外OTA,设备温度可能超过45度,这时候写入失败率明显上升。有些设备的Recovery里会读温度传感器,超过阈值就暂停安装并提示降温。这个逻辑需要厂商自己实现,AOSP默认没有。
我遇到过一台设备在OTA时因为温度过高导致eMMC进入保护模式,写入全部失败。后来在Recovery里加了温度检查,超过40度就降频写入,问题才解决。所以如果你的设备用在户外或者散热不好的环境,温度保护一定要做。
8.2 差分包生成时的基准版本管理
差分包的基准版本管理是个容易被忽视的坑。如果设备上的版本和生成差分包时的基准版本不一致,差分包就装不上。实际项目中,设备上的版本可能因为各种原因和官方版本有差异,比如用户刷了第三方模块、或者之前的OTA只成功了一半。
为了解决这个问题,OTA推送平台需要记录每台设备的当前版本,只推送和该版本匹配的差分包。如果设备版本不在支持列表里,就推全量包。这个逻辑听起来简单,但实现起来需要设备端和平台端的紧密配合。设备端要准确上报版本信息,平台端要维护版本和差分包的映射关系。
另外,差分包生成后要做验证。用基准版本镜像应用差分包,生成的新镜像要和目标版本镜像完全一致。这个验证能发现差分包生成过程中的错误,避免推送到设备后才失败。验证过程可以在服务器上做,不占用设备资源。
8.3 Recovery UI定制与自动化测试的平衡
很多厂商会定制Recovery的UI,加自己的logo和进度条。定制本身没问题,但要注意不要影响自动化测试。自动化测试通常通过解析Recovery的log来判断安装进度,如果UI定制改了log的输出格式,测试脚本就会失效。
我的建议是UI定制和log输出分离。UI可以随便改,但log的格式和关键字段要保持稳定。如果确实需要改log,要同步更新测试脚本。另外,Recovery的UI响应要快,不要因为绘制复杂图形导致按键响应延迟。我见过一个定制UI因为每帧都重绘整个屏幕,导致按键要按好几秒才有反应,用户体验很差。
自动化测试方面,可以用模拟按键或者adb命令来触发OTA安装,然后解析log判断结果。测试用例要覆盖全量包、差分包、签名失败、断电恢复等场景。每个场景都要有明确的预期结果,比如签名失败时log里要有特定的错误码。这样才能保证OTA流程的可靠性。