第三十讲:综合实战——从零搭建OTA升级系统、编写完整升级脚本、端到端升级验证
好,终于到了这一章。说实话,前面二十九章我们聊了那么多原理、分区、签名、差分包……但很多朋友可能心里还是没底:到底怎么把这些东西串起来?
这一讲,我们就来干一件实在事——从零搭建一个完整的OTA升级系统。我会带着你写一份能用的升级脚本,然后做一次端到端的升级验证。
嗯,我在项目里见过太多人,理论背得滚瓜烂熟,一到真机升级就翻车。所以这一章,咱们不玩虚的。
一、整体架构:OTA升级系统长什么样?
先看一张图,把整个流程理清楚。我习惯把OTA系统分成三块:服务端、客户端、升级引擎。
说白了,整个流程就是:服务端打一个包 → 客户端下载 → 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.0 和 v2.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);
}
}
五、端到端升级验证
好,现在包有了,脚本写了,客户端也调通了。怎么验证?我有一套标准流程:
5.1 验证步骤
- 环境准备:一台刷了v1.0的设备,adb连接正常
- 推送包:
adb push ota_update.zip /data/ota_package/ - 触发升级:
adb shell am broadcast -a android.intent.action.OTA_UPDATE(或直接调用API) - 观察日志:
adb logcat -s RecoverySystem:V OtaService:V - 等待重启:设备自动重启进入Recovery
- 查看升级日志:
adb shell cat /cache/recovery/last_log - 验证版本:
adb shell getprop ro.build.version.incremental
5.2 常见问题排查
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 升级卡在0% | 脚本挂载分区失败 | 检查分区节点是否存在 |
| 升级到100%重启后卡logo | boot分区写入错误 | 检查boot.img是否匹配 |
| 升级后版本号没变 | 槽位切换失败 | 检查update_engine日志 |
| 签名校验失败 | 密钥不匹配 | 重新生成包并签名 |
我的实战经验:第一次做端到端验证时,我建议你先做一次全量包升级,再做差分包。全量包成功率更高,能帮你快速验证整个链路是否通顺。等全量包跑通了,再折腾差分包。
另外,一定要保留升级日志。我每次升级完都会把 /cache/recovery/last_log 拉出来存档,万一出问题可以回溯。
六、总结
这一讲我们干了三件事:
- 搭建服务端:生成差分包,理解签名机制
- 手写升级脚本:从挂载到写入,每一步都讲清楚
- 端到端验证:从推包到重启,完整走一遍流程
说实话,OTA升级这件事,理论不难,但细节极多。我见过太多人栽在分区名写错、签名没配、脚本少了一行这些“小问题”上。嗯,希望你读完这一章,能少踩一些坑。
如果你在实际操作中遇到问题,欢迎来找我交流。记住:升级有风险,验证要谨慎。