11、Recovery 模式与 OTA:Recovery 工作原理、updater-script 编写、OTA 包生成

说到 Android 系统的升级,Recovery 模式绝对是绕不开的核心。我最早接触它是在做一款平板项目时,客户要求支持静默升级。结果第一次测试就把设备刷成了砖——嗯,从那以后我花了整整一周把 Recovery 的源码啃了一遍。

今天我们就来聊聊 Recovery 的工作原理、updater-script 怎么写,以及 OTA 包是怎么生成的。说白了,这三件事串起来就是一条完整的升级流水线。

Recovery 模式的工作原理

Recovery 本质上是一个独立的小系统。它有自己的内核、文件系统和 init 进程。你想想看,主系统都挂了,它还能干活,靠的就是这种独立性。

Recovery 的启动流程大致是这样的:

  1. Bootloader 检测到按键组合或系统标记
  2. 加载 Recovery 分区中的内核和 ramdisk
  3. init 进程启动,挂载 /cache 和 /data 分区
  4. 读取 /cache/recovery/command 文件中的指令
  5. 执行对应的操作(升级、清数据、打补丁等)

我个人习惯把 Recovery 看作一个「最小化的 Linux 系统 + 升级引擎」。它不跑 Android 框架,只做一件事:按照脚本把新系统写进分区。

核心要点:Recovery 不依赖主系统。它通过读取 /cache/recovery/command 文件来获取指令。这个文件由主系统的 update_engine 或用户手动写入。

我曾经踩过一个坑:在 /cache 分区空间不足时,Recovery 启动后直接崩溃。后来发现是 command 文件写入了一半就被截断了。所以我现在做项目时,都会在升级前检查 /cache 的剩余空间。

updater-script 编写

updater-script 是 OTA 包的灵魂。它用 Edify 语言编写,Recovery 中的 updater 进程会逐行解析执行。

先看一个最简单的例子:

# 挂载系统分区
mount("ext4", "EMMC", "/dev/block/bootdevice/by-name/system", "/system");

# 检查设备型号
assert(getprop("ro.product.device") == "mydevice" || 
       abort("This package is for device: mydevice; this device is " + getprop("ro.product.device") + "."));

# 写入系统镜像
block_image_update("/dev/block/bootdevice/by-name/system", package_extract_file("system.transfer.list"), "system.new.dat", "system.patch.dat");

# 写入 boot 镜像
write_raw_image("/dev/block/bootdevice/by-name/boot", package_extract_file("boot.img"));

# 设置权限
set_metadata("/system/bin/su", "uid", 0, "gid", 0, "mode", 06755);

# 卸载分区
unmount("/system");

这里有几个关键函数:

函数作用注意事项
mount/unmount挂载/卸载分区必须先挂载才能操作文件
assert条件检查失败会中止升级
block_image_update块级写入系统比文件级写入快很多
write_raw_image写入原始镜像用于 boot/recovery 分区
set_metadata设置文件权限替代旧的 set_perm

我的经验:写 updater-script 时,一定要在 assert 里检查设备型号。我见过太多因为刷错机型导致变砖的案例。另外,block_image_update 比 file_copy 快 3-5 倍,能用的地方尽量用。

为什么会用块级更新?你想想看,系统分区可能有几百 MB,如果逐文件复制,Recovery 得跑半天。块级更新直接对比新旧镜像的差异,只写入变化的部分。这就是增量 OTA 的核心原理。

OTA 包生成

OTA 包本质上是一个 ZIP 文件。它的目录结构是这样的:

my_ota_update.zip
├── META-INF/
│   ├── MANIFEST.MF
│   ├── CERT.RSA
│   ├── CERT.SF
│   └── com/
│       └── google/
│           └── android/
│               ├── updater-script
│               └── update-binary
├── system.new.dat
├── system.transfer.list
├── system.patch.dat
├── boot.img
└── recovery.img

生成 OTA 包的工具是 ota_from_target_files,它位于 AOSP 的 build/tools/releasetools/ 目录下。

基本用法:

# 先生成 target-files 包
make target-files-package

# 然后生成 OTA 包
./build/tools/releasetools/ota_from_target_files \
    -k build/target/product/security/testkey \
    -i previous_target_files.zip \
    out/target/product/mydevice/obj/PACKAGING/target_files_intermediates/mydevice-target_files-eng.zip \
    my_ota_update.zip

参数说明:

  • -k:指定签名密钥
  • -i:指定上一个版本的 target-files,用于生成增量包
  • 如果不加 -i,生成的是完整包

注意:签名密钥一定要保管好。我见过有团队把 release key 传到 Git 仓库里,结果被外部拿到后签了个恶意 ROM。建议使用独立的签名服务器,密钥不出机房。

生成过程中,工具会做以下几件事:

  1. 对比新旧 system 镜像,生成差异文件
  2. 打包 boot/recovery 镜像
  3. 生成 updater-script
  4. 对整个包进行签名

我个人习惯在生成 OTA 包后,先用 unzip -l 检查一下包内文件是否完整。特别是 updater-script,有时候会因为编码问题导致 Recovery 解析失败。

SVG 流程图:OTA 升级完整流程

OTA 升级完整流程 1. 生成 OTA 包 ota_from_target_files 2. 下载到 /cache 系统更新服务 3. 重启进 Recovery 写入 command 文件 4. Recovery 启动 读取 command → 执行升级 5. 解析 updater-script Edify 语言逐行执行 6. 写入分区 system / boot / recovery 7. 校验签名 验证包完整性 8. 重启主系统 清除升级标记 9. 升级完成 新系统正常运行 完整包:全量写入 | 增量包:只写入差异部分 正常流程 关键节点 完成状态

避坑指南

做 OTA 升级这么多年,我总结了几条血泪教训:

  • 签名一致性:Recovery 和 OTA 包必须用同一套密钥签名。我曾经因为测试密钥和正式密钥混用,导致升级到一半签名校验失败。
  • 分区大小:升级前一定要检查目标分区是否有足够空间。特别是 system 分区,如果新镜像比分区大,升级会直接失败。
  • 断电保护:Recovery 升级过程中断电,设备大概率变砖。我建议在硬件层面加一个升级指示灯,让用户知道正在升级不要断电。
  • 回滚机制:永远保留上一个版本的备份。我习惯在升级前把旧系统备份到 /cache 分区,万一新系统起不来还能回滚。

我的小技巧:调试 updater-script 时,可以在脚本开头加一句 ui_print("debug: step 1");,这样 Recovery 的日志里就能看到执行到哪一步了。比瞎猜快得多。

嗯,Recovery 和 OTA 这块内容就这些。核心就是理解 Recovery 的独立运行机制,写好 updater-script,然后用工具生成正确的 OTA 包。这三步走通了,升级系统就不是什么难事了。


公众号:蓝海资料掘金营,微信 deep3321