29、OTA升级实战:从AOSP编译到OTA包生成与升级

这一章,咱们来点真格的。

前面讲了那么多动态分区的原理、虚拟快照、COW机制,说实话,都是纸上谈兵。今天我们就从AOSP源码编译开始,一路走到生成OTA包,最后在设备上完成升级。我当年第一次完整跑通这个流程时,整整折腾了三天三夜。嗯,有些坑,我希望你能绕过去。

29.1 编译AOSP:你得先有个系统

OTA升级的前提是什么?你得有两个系统版本——一个旧的,一个新的。所以第一步,编译AOSP。

我个人习惯用 lunch 选择目标设备。比如我们模拟一个Pixel设备:

source build/envsetup.sh
lunch aosp_arm64-userdebug
make -j16

这里有个小细节:userdebugeng 版本会开启更多调试功能,但如果你要做正式的OTA测试,我建议用 user 版本。为什么?因为 user 版本更接近真实用户环境,有些权限问题在 userdebug 下根本暴露不出来。

编译完成后,你会得到这些关键产物:

  • out/target/product/<device>/system.img —— 系统镜像
  • out/target/product/<device>/vendor.img —— 厂商镜像
  • out/target/product/<device>/super.img —— 超级分区镜像(动态分区)
  • out/target/product/<device>/obj/PACKAGING/target_files_intermediates/ —— target_files.zip

注意最后一个,target_files.zip 是生成OTA包的原材料。它里面包含了所有分区的原始数据、文件权限、SELinux上下文等信息。说白了,它就是你的系统快照。

我的经验:第一次编译时,记得把 out 目录备份一份。我当年就是忘了备份,改了点代码重新编译后,发现旧版本没了,想生成增量OTA包都生成不了。血的教训。

29.2 生成OTA包:增量 vs 全量

有了两个版本的 target_files.zip,我们就可以生成OTA包了。AOSP提供了 ota_from_target_files 这个Python脚本,路径在 build/tools/releasetools/ 下。

29.2.1 全量OTA包

全量包最简单,就是把整个系统打包成一个更新包。设备收到后,不管当前是什么版本,直接覆盖所有分区。

./build/tools/releasetools/ota_from_target_files \
    -k build/target/product/security/testkey \
    out/target/product/<device>/obj/PACKAGING/target_files_intermediates/<device>-target_files-1.0.zip \
    ota_full_1.0.zip

全量包的优点是简单、可靠。缺点是包体积大。一个完整的super.img可能有4-6GB,压缩后也有1-2GB。用户下载起来很痛苦。

29.2.2 增量OTA包

增量包才是OTA的精髓。它只包含两个版本之间的差异数据。

./build/tools/releasetools/ota_from_target_files \
    -i out/target/product/<device>/obj/PACKAGING/target_files_intermediates/<device>-target_files-1.0.zip \
    -k build/target/product/security/testkey \
    out/target/product/<device>/obj/PACKAGING/target_files_intermediates/<device>-target_files-2.0.zip \
    ota_incremental_1.0_to_2.0.zip

这里 -i 参数指定了旧版本的target_files。脚本会逐文件对比新旧版本,只打包发生变化的部分。

核心原理:增量包内部其实是一个BSDiff补丁。对于二进制文件,它用BSDiff算法计算差异;对于文本文件,用普通的diff。更新时,recovery系统会读取补丁,应用到旧文件上,生成新文件。

你想想看,如果系统版本从1.0升级到1.1,可能只改了十几个文件。增量包可能只有几十MB。用户体验好太多了。

29.3 OTA包的结构:里面到底有什么?

生成的OTA包其实是一个ZIP文件。我们解开来看看:

unzip -l ota_incremental_1.0_to_2.0.zip

Archive:  ota_incremental_1.0_to_2.0.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
     1234  2024-01-15 10:00   META-INF/com/android/metadata
    56789  2024-01-15 10:00   META-INF/com/android/otacert
  1234567  2024-01-15 10:00   payload.bin
      789  2024-01-15 10:00   payload_properties.txt
      456  2024-01-15 10:00   care_map.pb

我来解释一下每个文件的作用:

文件 作用
META-INF/com/android/metadata 元数据,包含版本号、设备类型、时间戳等
META-INF/com/android/otacert OTA包的签名证书,用于验证包的真实性
payload.bin 核心数据,包含所有分区的更新内容
payload_properties.txt payload的属性,如大小、哈希值、操作类型等
care_map.pb Protobuf格式的"关心映射",标记哪些块需要更新

这里重点说一下 payload.bin。它内部是一个流式格式,包含多个操作:

  • SOURCE_COPY:从旧分区直接复制数据,不做修改
  • SOURCE_BSDIFF:对旧数据应用BSDiff补丁
  • BROTLI_BSDIFF:用Brotli压缩的BSDiff补丁(Android 10+)
  • ZERO:写入全零数据
  • DISCARD:丢弃数据(用于trim)

我曾经调试过一个奇怪的问题:增量包升级后,某个应用总是崩溃。后来发现是 care_map.pb 漏掉了一个块,导致那个块没有被更新,还是旧数据。嗯,从那以后我每次都会检查care_map的完整性。

29.4 升级流程:从下载到重启

OTA包生成好了,怎么升级?流程大致如下:

  1. 下载:系统下载OTA包,通常放在 /data/ota_package/
  2. 校验:验证签名和哈希,防止篡改
  3. 重启到recovery:设置 bootloader_message,然后重启
  4. 应用更新:recovery系统解析payload.bin,逐个分区更新
  5. 重启到新系统:更新成功,重启进入新版本

在动态分区设备上,第4步会涉及 update_enginesnapshotctl 的配合。具体来说:

# update_engine 内部执行流程(简化版)
1. 读取 payload.bin 的操作列表
2. 对于每个分区操作:
   a. 如果是 SOURCE_COPY:直接从旧分区读取数据,写入新分区槽位
   b. 如果是 SOURCE_BSDIFF:读取旧数据,应用补丁,写入新分区槽位
3. 所有分区更新完成后,调用 snapshotctl 创建快照
4. 设置新槽位为 active
5. 重启
注意:在动态分区设备上,super 分区本身不会被更新。更新的是 super 内部的逻辑分区(system、vendor、product等)。super 分区的布局信息存储在 metadata 分区中,这个分区在OTA过程中会被更新。

29.5 实战中的坑:我踩过的那些雷

讲几个我实际项目中遇到的问题,希望能帮你省点时间。

坑一:签名不匹配

我曾经在生成OTA包时用了测试密钥,但设备上烧录的是正式密钥。结果升级时签名校验失败,直接卡在recovery界面。解决办法:确保生成OTA包和烧录系统时使用同一套密钥。

坑二:分区大小不足

动态分区的好处是灵活,但如果你在OTA中新增了一个分区,而 super 分区的剩余空间不够,升级就会失败。我建议在编译时预留10%-15%的额外空间。

坑三:AB切换失败

有一次升级完成后,设备反复重启。查了半天,发现是 boot_control HAL 实现有问题,导致槽位切换不成功。这个坑比较隐蔽,建议在升级完成后手动检查 /proc/bootconfig 确认当前槽位。

29.6 知识体系总览

下面这张图总结了OTA升级的完整流程和关键组件:

OTA升级实战流程 AOSP编译 make -j16 → target_files.zip 生成OTA包 ota_from_target_files OTA包结构 payload.bin + metadata 升级流程 下载 → 校验 → Recovery → 应用 → 重启 关键组件 update_engine + snapshotctl 动态分区更新细节 super分区(不变) 逻辑分区(更新) metadata分区(更新) COW快照 BSDiff补丁 → 应用到旧分区数据 → 生成新分区数据 → 写入新槽位

这张图把整个流程串起来了。从左到右是时间线,从上到下是层次关系。你可以看到,编译阶段产出target_files,然后生成OTA包,最后在设备上执行升级流程。底部是动态分区特有的细节——super分区本身不变,但内部的逻辑分区和metadata会被更新。

29.7 验证升级结果

升级完成后,怎么确认成功了?我一般做这几步检查:

# 1. 检查当前槽位
getprop ro.boot.slot_suffix
# 输出应该是 _a 或 _b

# 2. 检查系统版本
getprop ro.build.version.incremental
# 应该等于新版本的增量版本号

# 3. 检查动态分区状态
snapshotctl dump
# 查看是否有残留的快照

# 4. 检查分区挂载情况
mount | grep -E "system|vendor|product"
# 确认所有分区都正确挂载

如果一切正常,恭喜你,OTA升级成功了。

说实话,第一次完整跑通这个流程时,那种成就感是很强的。从源码编译到生成OTA包,再到设备上升级成功,整个链路都掌握在自己手里。嗯,这就是做系统底层开发的乐趣所在。

最后一个小建议:在实际项目中,建议搭建自动化CI/CD流水线。每次代码提交后自动编译、自动生成OTA包、自动在测试设备上升级。手动操作太容易出错了,我吃过这个亏。

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