11、Recovery 模式与 OTA:Recovery 工作原理、updater-script 编写、OTA 包生成
说到 Android 系统的升级,Recovery 模式绝对是绕不开的核心。我最早接触它是在做一款平板项目时,客户要求支持静默升级。结果第一次测试就把设备刷成了砖——嗯,从那以后我花了整整一周把 Recovery 的源码啃了一遍。
今天我们就来聊聊 Recovery 的工作原理、updater-script 怎么写,以及 OTA 包是怎么生成的。说白了,这三件事串起来就是一条完整的升级流水线。
Recovery 模式的工作原理
Recovery 本质上是一个独立的小系统。它有自己的内核、文件系统和 init 进程。你想想看,主系统都挂了,它还能干活,靠的就是这种独立性。
Recovery 的启动流程大致是这样的:
- Bootloader 检测到按键组合或系统标记
- 加载 Recovery 分区中的内核和 ramdisk
- init 进程启动,挂载 /cache 和 /data 分区
- 读取 /cache/recovery/command 文件中的指令
- 执行对应的操作(升级、清数据、打补丁等)
我个人习惯把 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。建议使用独立的签名服务器,密钥不出机房。
生成过程中,工具会做以下几件事:
- 对比新旧 system 镜像,生成差异文件
- 打包 boot/recovery 镜像
- 生成 updater-script
- 对整个包进行签名
我个人习惯在生成 OTA 包后,先用 unzip -l 检查一下包内文件是否完整。特别是 updater-script,有时候会因为编码问题导致 Recovery 解析失败。
SVG 流程图:OTA 升级完整流程
避坑指南
做 OTA 升级这么多年,我总结了几条血泪教训:
- 签名一致性:Recovery 和 OTA 包必须用同一套密钥签名。我曾经因为测试密钥和正式密钥混用,导致升级到一半签名校验失败。
- 分区大小:升级前一定要检查目标分区是否有足够空间。特别是 system 分区,如果新镜像比分区大,升级会直接失败。
- 断电保护:Recovery 升级过程中断电,设备大概率变砖。我建议在硬件层面加一个升级指示灯,让用户知道正在升级不要断电。
- 回滚机制:永远保留上一个版本的备份。我习惯在升级前把旧系统备份到 /cache 分区,万一新系统起不来还能回滚。
我的小技巧:调试 updater-script 时,可以在脚本开头加一句 ui_print("debug: step 1");,这样 Recovery 的日志里就能看到执行到哪一步了。比瞎猜快得多。
嗯,Recovery 和 OTA 这块内容就这些。核心就是理解 Recovery 的独立运行机制,写好 updater-script,然后用工具生成正确的 OTA 包。这三步走通了,升级系统就不是什么难事了。