22、OTA升级与Recovery系统:AB分区、update_engine、Recovery模式与脚本
OTA升级,说白了就是手机系统空中升级。你手机收到推送,点一下“立即更新”,重启后系统就变了——这背后其实是一整套底层机制在运作。我最早接触这块时,觉得不就是个拷贝文件吗?后来踩了坑才发现,这里面的门道深着呢。
22.1 从传统升级到AB分区:为什么要有AB分区?
传统Android升级有个致命问题:升级过程中如果断电、死机,手机就变砖了。你想想看,系统正在擦写system分区,啪一下断电了,系统文件写到一半——嗯,这手机基本就废了。
Google后来搞出了AB分区机制,也叫无缝升级。简单说就是系统准备了两套分区:slot A和slot B。一套在跑,另一套闲着。升级时,系统把新固件写入空闲的那套分区,写完后标记为“可用”,下次重启就切过去。
核心思想:升级过程中,当前运行的系统不受任何影响。即使升级失败,系统还能从旧分区正常启动。
我在项目中遇到过一件事:某款手机用传统升级方案,用户升级到一半电池耗尽,结果手机无法开机。售后返修率飙升。后来换成AB分区,这种问题再没出现过。
22.2 AB分区的布局与关键分区
AB分区不是简单地把system复制一份。它涉及多个关键分区,每个都有对应的A/B副本:
| 分区名 | 作用 | AB版本 |
|---|---|---|
| boot | 内核与ramdisk | boot_a / boot_b |
| system | 系统镜像 | system_a / system_b |
| vendor | 厂商专有库 | vendor_a / vendor_b |
| product | 产品定制内容 | product_a / product_b |
| misc | 启动控制信息 | 无AB版本 |
这里有个关键点:misc分区没有AB版本。它存的是当前应该从哪个slot启动的信息。Bootloader启动时会读misc分区,决定加载boot_a还是boot_b。
个人习惯:调试AB分区时,我经常用 adb shell getprop ro.boot.slot_suffix 查看当前激活的是哪个slot。这个命令能帮你快速定位问题。
22.3 update_engine:升级的核心引擎
update_engine是Android AB升级的幕后主角。它运行在后台,负责下载、校验、写入新系统。我刚开始看它的代码时,觉得这玩意儿就是个下载器加写入器。后来仔细研究才发现,它的状态机设计得非常精巧。
update_engine的工作流程大致如下:
- 下载阶段:从OTA服务器拉取增量包或全量包
- 校验阶段:验证签名和哈希,防止篡改
- 写入阶段:将新数据写入非活跃slot
- 标记阶段:写入成功后,将新slot标记为可启动
- 切换阶段:重启后Bootloader加载新slot
你可能会问:增量包是怎么工作的?说白了,增量包只包含新旧版本之间的差异数据。update_engine会读取旧分区的内容,和增量包中的补丁合并,生成完整的新分区数据。这样下载量小很多。
我曾经踩过一个坑:某次升级时,增量包中的补丁数据有误,导致合并后的system分区校验失败。update_engine自动回滚到旧slot,用户完全没察觉。这就是AB分区的优势——失败也不影响使用。
22.4 Recovery模式:最后的救命稻草
Recovery模式是Android的急救系统。它独立于主系统运行,有自己的内核和ramdisk。当主系统挂了,Reco模式还能救你一把。
Recovery模式的启动流程:
- Bootloader检测到特殊按键组合(比如音量上+电源键)
- 或者misc分区中有“进入Recovery”的标记
- Bootloader加载recovery分区中的内核和ramdisk
- Recovery系统启动,读取/cache/recovery/目录下的命令脚本
Recovery模式的核心是它执行的脚本。这些脚本存放在 /cache/recovery/command 文件中。比如:
--update_package=/sdcard/update.zip
--wipe_data
--wipe_cache
每一行都是一个命令。Recovery系统启动后,会逐行解析执行。我调试时经常手动创建这个文件,模拟各种升级场景。
22.5 Recovery脚本与edify语言
Recovery模式下执行的脚本是用edify语言写的。edify是一种声明式脚本语言,专门用于描述文件系统操作。你看到的 update.zip 包里的 META-INF/com/google/android/updater-script 就是edify脚本。
一个典型的edify脚本片段:
mount("ext4", "EMMC", "/dev/block/bootdevice/by-name/system", "/system");
package_extract_dir("system", "/system");
set_metadata("/system/bin/surfaceflinger", "uid", 1000, "gid", 1000, "mode", 0755);
unmount("/system");
这段脚本做了三件事:挂载system分区、把包里的system目录解压进去、设置文件权限。最后卸载分区。
我建议:调试Recovery脚本时,可以用 adb reboot recovery 进入Recovery模式,然后用 adb shell 进去手动执行命令。这样能快速验证脚本逻辑是否正确。
22.6 AB分区与Recovery的协作关系
很多人以为AB分区和Recovery模式是互斥的。其实不是。AB分区升级时,Recovery模式仍然有用武之地。
比如:AB升级过程中,如果update_engine发现新slot写入失败,它会回滚。但如果两个slot都坏了怎么办?这时候就需要Recovery模式来救场了。Recovery模式可以从外部存储刷入完整的系统镜像。
下面这张图展示了AB分区升级的完整流程:
22.7 实战:手动触发AB升级
调试时,我经常手动触发升级来验证流程。这里分享一个我常用的方法:
# 查看当前slot
adb shell getprop ro.boot.slot_suffix
# 手动触发update_engine
adb shell update_engine_client --payload file:///sdcard/ota_package.zip \
--update \
--headers="--metadata_size=1024"
# 查看升级状态
adb shell update_engine_client --status
# 重启切换到新slot
adb reboot
注意:手动触发时,payload参数指向的OTA包必须放在设备内部存储中。我曾经犯过一个低级错误——把包放在外部SD卡上,结果update_engine死活读不到。
重要提醒:生产环境不要手动触发升级。update_engine有严格的版本校验和签名验证,手动操作容易破坏系统完整性。调试时建议用工程版本固件。
22.8 总结与避坑指南
AB分区和Recovery系统,是Android底层升级的两大支柱。AB分区保证了升级的安全性,Recovery模式提供了最后的兜底方案。
最后分享几个我踩过的坑:
- 分区大小不匹配:AB分区要求A和B的大小完全一致。我遇到过vendor分区A比B大几个扇区,导致写入失败。解决办法是重新分区。
- 增量包依赖旧版本:增量包只能从特定版本升级。用户如果跳版本升级,增量包会校验失败。这时候必须用全量包。
- Recovery脚本权限问题:edify脚本中设置文件权限时,uid和gid必须用数字。用字符串名称会导致解析失败。
嗯,OTA升级这块内容就讲到这里。记住一句话:升级无小事,安全第一。AB分区和Recovery模式就是Android给你上的双保险。