第三十讲:综合实战——从零搭建OTA升级系统、编写完整升级脚本、端到端升级验证

好,终于到了这一章。说实话,前面二十九章我们聊了那么多原理、分区、签名、差分包……但很多朋友可能心里还是没底:到底怎么把这些东西串起来?

这一讲,我们就来干一件实在事——从零搭建一个完整的OTA升级系统。我会带着你写一份能用的升级脚本,然后做一次端到端的升级验证。

嗯,我在项目里见过太多人,理论背得滚瓜烂熟,一到真机升级就翻车。所以这一章,咱们不玩虚的。


一、整体架构:OTA升级系统长什么样?

先看一张图,把整个流程理清楚。我习惯把OTA系统分成三块:服务端、客户端、升级引擎

OTA升级系统整体架构 服务端 OTA包生成工具 签名 & 校验 版本管理 客户端 升级检测 下载管理 升级触发 升级引擎 Recovery模式 脚本解析 分区写入 ① 生成差分包 ② 下载到 /data/ota_package/ ③ 重启进入Recovery 端到端流程:构建包 → 推送 → 下载 → 校验 → 重启 → 升级 → 验证 ⚠ 关键:升级脚本必须与分区表严格匹配 否则轻则升级失败,重则变砖

说白了,整个流程就是:服务端打一个包 → 客户端下载 → Recovery执行脚本 → 分区写入 → 重启验证。每一步都有坑,我们一个一个填。


二、搭建服务端:生成OTA升级包

服务端的工作,核心就是生成 update.zip。Android官方提供了 ota_from_target_files 脚本,但我个人习惯自己写一套构建流程,更可控。

2.1 准备目标文件

首先,你得有编译好的 target_files.zip。这个包包含了所有分区镜像、文件列表、签名信息。

# 编译生成 target_files.zip
source build/envsetup.sh
lunch aosp_arm64-userdebug
make target-files-package -j8

# 输出路径:out/target/product/你的设备/obj/PACKAGING/target_files_intermediates/
# 文件名:aosp_arm64-target_files-eng.xxx.zip

嗯,这里要注意:一定要用 userdebug 或 eng 版本,user 版本会缺少一些调试工具,后面验证时会很痛苦。我曾经因为这个折腾了一整天。

2.2 生成差分包

假设你有两个版本:v1.0v2.0。我们要生成从 v1.0 升级到 v2.0 的差分包。

# 生成差分包
./build/tools/releasetools/ota_from_target_files \
    -i v1.0-target_files.zip \
    -k build/target/product/security/testkey \
    v2.0-target_files.zip \
    ota_update.zip

参数说明:

参数含义
-i指定旧版本 target_files
-k签名密钥,默认用 testkey
最后一个参数新版本 target_files
输出ota_update.zip(即差分包)
我的习惯:生成包之后,先解压看一眼里面的 META-INF/com/google/android/updater-script。这个脚本就是升级的“剧本”,后面我们会手写一个。

三、编写升级脚本:updater-script 深度解析

升级脚本是OTA的灵魂。它运行在Recovery模式下,负责执行所有分区操作。

我见过最惨的翻车现场:脚本里写错了分区名,结果把boot分区写到了system分区上……嗯,那台设备直接变砖,只能拆机用烧录器救。

3.1 脚本基本结构

# updater-script 示例
# 第一部分:挂载分区
mount("ext4", "EMMC", "/dev/block/by-name/system", "/system");
mount("ext4", "EMMC", "/dev/block/by-name/vendor", "/vendor");

# 第二部分:校验签名
assert(verify_package("ota_update.zip"));

# 第三部分:写入分区
block_image_update("/dev/block/by-name/system",
    package_extract_file("system.transfer.list"),
    package_extract_file("system.new.dat.br"),
    package_extract_file("system.patch.dat"));

block_image_update("/dev/block/by-name/vendor",
    package_extract_file("vendor.transfer.list"),
    package_extract_file("vendor.new.dat.br"),
    package_extract_file("vendor.patch.dat"));

# 第四部分:更新boot
write_raw_image("/dev/block/by-name/boot", "boot.img");

# 第五部分:设置属性
set_progress(1.0);

3.2 手写一个完整升级脚本

假设我们的设备分区如下:

分区设备节点挂载点
system/dev/block/by-name/system/system
vendor/dev/block/by-name/vendor/vendor
boot/dev/block/by-name/boot
dtbo/dev/block/by-name/dtbo

下面是我手写的一个完整脚本,加了详细的注释:

# 完整升级脚本 - 适用于A/B分区设备
# 作者:资深工程师的实战模板

# 1. 断言:检查设备型号
assert(getprop("ro.product.device") == "my_device" ||
       abort("E3004: This package is for my_device devices; this is a " +
             getprop("ro.product.device") + "."));

# 2. 挂载分区(注意:A/B设备需要挂载当前槽位)
mount("ext4", "EMMC", "/dev/block/by-name/system_a", "/system", "");
mount("ext4", "EMMC", "/dev/block/by-name/vendor_a", "/vendor", "");

# 3. 显示进度
ui_print("正在验证升级包...");
show_progress(0.1, 10);

# 4. 校验包完整性
assert(verify_package("ota_update.zip"));

# 5. 更新system分区
ui_print("正在更新系统分区...");
show_progress(0.4, 120);
block_image_update("/dev/block/by-name/system_a",
    package_extract_file("system.transfer.list"),
    package_extract_file("system.new.dat.br"),
    package_extract_file("system.patch.dat"));

# 6. 更新vendor分区
ui_print("正在更新vendor分区...");
show_progress(0.2, 60);
block_image_update("/dev/block/by-name/vendor_a",
    package_extract_file("vendor.transfer.list"),
    package_extract_file("vendor.new.dat.br"),
    package_extract_file("vendor.patch.dat"));

# 7. 更新boot镜像
ui_print("正在更新boot分区...");
show_progress(0.1, 30);
write_raw_image("/dev/block/by-name/boot_a", "boot.img");

# 8. 更新dtbo
ui_print("正在更新dtbo分区...");
write_raw_image("/dev/block/by-name/dtbo_a", "dtbo.img");

# 9. 切换槽位(A/B设备关键步骤)
ui_print("正在切换启动槽位...");
run_program("/sbin/update_engine_client", "--payload", "file:///data/ota_package/ota_update.zip",
            "--update", "--switch_slot=true");

# 10. 完成
ui_print("升级完成!即将重启...");
set_progress(1.0);
⚠ 避坑指南:我曾经在脚本里漏掉了 verify_package 这一步,结果一个损坏的包被刷了进去,系统启动时直接卡在bootloader。从那以后,我每条脚本都会加上签名校验,绝不偷懒。

四、客户端实现:下载与触发升级

客户端负责从服务端拉取升级包,然后触发Recovery升级。这里我推荐用 UpdateEngine 或者直接调用 RecoverySystem API。

4.1 下载升级包

// Java代码:下载升级包到指定目录
public class OtaDownloader {
    private static final String OTA_PATH = "/data/ota_package/ota_update.zip";
    
    public boolean downloadPackage(String url) {
        try {
            URL otaUrl = new URL(url);
            HttpURLConnection conn = (HttpURLConnection) otaUrl.openConnection();
            conn.setRequestMethod("GET");
            conn.connect();
            
            // 检查文件大小
            int fileSize = conn.getContentLength();
            if (fileSize < 1024 * 1024) { // 小于1MB,明显不对
                Log.e("OTA", "文件大小异常,可能下载失败");
                return false;
            }
            
            // 流式写入
            InputStream is = conn.getInputStream();
            FileOutputStream fos = new FileOutputStream(OTA_PATH);
            byte[] buffer = new byte[8192];
            int len;
            while ((len = is.read(buffer)) != -1) {
                fos.write(buffer, 0, len);
            }
            fos.close();
            is.close();
            
            // 校验MD5(可选)
            return verifyMd5(OTA_PATH, expectedMd5);
        } catch (Exception e) {
            Log.e("OTA", "下载失败", e);
            return false;
        }
    }
}

4.2 触发升级

// 触发Recovery升级
public void triggerUpdate() {
    try {
        // 方式一:使用RecoverySystem(推荐)
        RecoverySystem.installPackage(context, new File(OTA_PATH));
        
        // 方式二:直接写misc分区(不推荐,容易出错)
        // writeMiscCommand("--update_package=" + OTA_PATH);
        
        // 重启进入Recovery
        PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
        pm.reboot("recovery");
    } catch (IOException e) {
        Log.e("OTA", "触发升级失败", e);
    }
}
小技巧:触发升级前,最好先检查一下电池电量。我一般要求电量大于50%才允许升级,否则中途断电就真成砖了。

五、端到端升级验证

好,现在包有了,脚本写了,客户端也调通了。怎么验证?我有一套标准流程:

5.1 验证步骤

  1. 环境准备:一台刷了v1.0的设备,adb连接正常
  2. 推送包adb push ota_update.zip /data/ota_package/
  3. 触发升级adb shell am broadcast -a android.intent.action.OTA_UPDATE(或直接调用API)
  4. 观察日志adb logcat -s RecoverySystem:V OtaService:V
  5. 等待重启:设备自动重启进入Recovery
  6. 查看升级日志adb shell cat /cache/recovery/last_log
  7. 验证版本adb shell getprop ro.build.version.incremental

5.2 常见问题排查

现象可能原因解决方法
升级卡在0%脚本挂载分区失败检查分区节点是否存在
升级到100%重启后卡logoboot分区写入错误检查boot.img是否匹配
升级后版本号没变槽位切换失败检查update_engine日志
签名校验失败密钥不匹配重新生成包并签名

我的实战经验:第一次做端到端验证时,我建议你先做一次全量包升级,再做差分包。全量包成功率更高,能帮你快速验证整个链路是否通顺。等全量包跑通了,再折腾差分包。

另外,一定要保留升级日志。我每次升级完都会把 /cache/recovery/last_log 拉出来存档,万一出问题可以回溯。


六、总结

这一讲我们干了三件事:

  • 搭建服务端:生成差分包,理解签名机制
  • 手写升级脚本:从挂载到写入,每一步都讲清楚
  • 端到端验证:从推包到重启,完整走一遍流程

说实话,OTA升级这件事,理论不难,但细节极多。我见过太多人栽在分区名写错、签名没配、脚本少了一行这些“小问题”上。嗯,希望你读完这一章,能少踩一些坑。

如果你在实际操作中遇到问题,欢迎来找我交流。记住:升级有风险,验证要谨慎


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