4、升级脚本语法:Edify脚本语言基础、getprop与assert断言、mount与format操作、ui_print与show_progress

好,咱们今天聊聊升级脚本。说白了,它就是告诉Android系统「怎么把新系统装进去」的那份说明书。我刚开始接触OTA开发时,觉得这玩意儿不就是一堆命令吗?后来踩了几个坑才明白——脚本写不好,升级就翻车。嗯,咱们一步步来。

4.1 Edify脚本语言基础

Edify是Android Recovery模式下专用的脚本语言。它不像Python那么灵活,也不像Shell那么通用。但它专为系统升级设计,简单、安全、可控。

一个典型的Edify脚本长这样:

assert(getprop("ro.product.device") == "sailfish" ||
       getprop("ro.build.product") == "sailfish");
mount("ext4", "EMMC", "/dev/block/bootdevice/by-name/system", "/system");
show_progress(0.750000, 0);
ui_print("正在刷写系统分区...");
package_extract_dir("system", "/system");
unmount("/system");

你看,它没有复杂的循环和条件判断。Edify的设计哲学就是「线性执行,失败即停」。我在项目中遇到过有人想用Edify写循环逻辑,结果发现根本行不通——它就不是干这个的。

核心特点:

  • 基于栈的表达式求值
  • 所有操作都是函数调用形式
  • 不支持变量赋值(没有等号)
  • 不支持循环和跳转
  • 函数返回值直接参与表达式

Edify的函数分为几类:文件操作、分区操作、属性读取、UI控制、脚本控制。咱们今天重点讲后面几个。

4.2 getprop与assert断言

getprop函数用来读取系统属性。在升级脚本里,它最常见的用途就是做设备兼容性检查。

getprop("ro.product.device")
getprop("ro.build.fingerprint")
getprop("ro.build.version.sdk")

assert函数是Edify里的「守门员」。它接受一个布尔表达式,如果结果为false,脚本立即终止,并返回错误信息。

我见过最经典的用法是这样的:

assert(getprop("ro.product.device") == "beryllium" ||
       getprop("ro.build.product") == "beryllium");

为什么要用两个属性?因为不同厂商对设备名的存储方式不一样。有的用ro.product.device,有的用ro.build.product。我早期做OTA时只检查了第一个,结果某厂商的定制ROM直接刷错了机型——嗯,那次教训挺深刻的。

避坑指南:

我曾经在assert里写了一个复杂的条件链,结果因为括号位置不对,导致条件永远为true。刷完才发现机型不对。后来我养成了一个习惯:在assert里只写简单的比较,复杂的逻辑拆成多个assert。

除了机型检查,assert还常用于版本号校验:

assert(getprop("ro.build.version.sdk") >= "28");
assert(less_than_int(getprop("ro.build.date.utc"), 1609459200));

less_than_int是Edify内置的比较函数。你想想看,如果没有它,你拿字符串比时间戳,那结果可就乱套了。

4.3 mount与format操作

mount和format是升级脚本里最危险的操作。用错了,手机变砖。用对了,升级顺利。

先看mount的语法:

mount(fs_type, partition_type, device, mount_point);

实际例子:

mount("ext4", "EMMC", "/dev/block/bootdevice/by-name/system", "/system");

这里有个细节:mount_point必须是绝对路径,而且这个路径在Recovery模式下必须存在。我遇到过有人忘了创建/system目录,结果mount失败,脚本直接退出。

format的用法更直接:

format("ext4", "EMMC", "/dev/block/bootdevice/by-name/userdata", "0", "/data");

最后一个参数是挂载点,用来告诉format函数「我要格式化的是哪个分区上的文件系统」。这个参数可以省略,但我不建议你省略——万一你写错了设备节点,至少还有个挂载点做二次校验。

警告:

format操作不可逆!我曾经在调试脚本时,不小心把system分区格式化了,结果Recovery模式直接崩溃——因为/system/bin下的工具全没了。从那以后,我每次写format之前都会反复确认设备节点路径。

mount和format的常见组合:

# 先格式化,再挂载
format("ext4", "EMMC", "/dev/block/bootdevice/by-name/system", "0", "/system");
mount("ext4", "EMMC", "/dev/block/bootdevice/by-name/system", "/system");

# 或者只挂载不格式化(增量升级时常用)
mount("ext4", "EMMC", "/dev/block/bootdevice/by-name/system", "/system");

你想想看,什么时候需要格式化?完整包升级时。什么时候只需要挂载?增量包升级时。这个区分很重要,搞错了轻则升级失败,重则数据全丢。

4.4 ui_print与show_progress

这两个函数负责给用户看进度。ui_print输出文本,show_progress控制进度条。

ui_print("正在准备升级环境...");
show_progress(0.1, 10);

ui_print("正在刷写系统分区...");
show_progress(0.7, 120);

ui_print("正在刷写数据分区...");
show_progress(0.2, 60);

show_progress的第一个参数是进度占比(0到1之间),第二个参数是预估时间(秒)。注意,这个时间只是给用户看的,实际执行时间由脚本决定。

我个人的习惯是:把进度分成几个大段,每段内部再细分。比如:

# 总进度 = 1.0
# 准备阶段 10%
show_progress(0.1, 10);
ui_print("检查设备兼容性...");
# ... 实际检查代码

# 刷写阶段 70%
show_progress(0.7, 120);
ui_print("刷写系统文件...");
# ... 实际刷写代码

# 收尾阶段 20%
show_progress(0.2, 30);
ui_print("清理临时文件...");
# ... 实际清理代码

实用技巧:

ui_print的字符串里可以包含换行符\n。我经常用它来输出多行信息,比如升级前后的版本号对比。这样用户一眼就能看到「从哪个版本升到了哪个版本」。

还有一个细节:ui_print的输出会写入Recovery的日志文件。所以调试时,你可以用ui_print来打log,方便排查问题。我曾经在排查一个升级失败的问题时,就是在关键步骤前后加了ui_print,才定位到是某个文件解压失败了。

4.5 知识体系总览

下面这张图帮你理清今天讲的内容:

Edify升级脚本知识体系 Edify语言基础 线性执行·无变量·函数式 getprop & assert 设备校验·版本检查 mount & format 挂载·格式化·分区管理 ui_print & show_progress 用户反馈·进度显示 栈式表达式求值 函数调用即操作 ro.product.device ro.build.fingerprint 版本号比较 设备节点路径 文件系统类型 完整包 vs 增量包 进度占比分配 预估时间设置 日志输出调试 核心原则:线性执行 · 失败即停 · 安全第一

这张图把今天讲的四个模块串起来了。你从左边往右看:先学语言基础,再学属性读取和断言,然后是分区操作,最后是UI反馈。这四个模块组合起来,就是一个完整的升级脚本。

好了,今天的内容就到这里。Edify脚本看起来简单,但写好了不容易。我建议你动手写一个完整的升级脚本,从assert开始,到ui_print结束,把每个函数都用一遍。只有亲手写过,才能真正理解这些函数之间的配合关系。


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