OTA差分镜像压缩与下载传输优化 - 第2节
压缩算法基础:无损压缩与有损压缩的区别
聊压缩之前,咱们得先搞清楚一个根本问题:你到底能不能容忍数据丢失?
OTA 升级里传的是固件镜像、系统分区、应用程序。这些东西,丢一个 bit 都可能让设备变砖。所以我个人习惯,在 OTA 场景下几乎只考虑无损压缩。
无损压缩 vs 有损压缩
说白了,区别就一句话:
- 无损压缩:压缩完解出来,跟原来一模一样。一个字节都不差。
- 有损压缩:扔掉一些人眼/人耳不敏感的信息,换更高的压缩率。解出来已经不是原来的数据了。
我举个例子你就明白了。你手机拍的照片,存成 JPEG 就是有损压缩——文件小了,但放大看有马赛克。你写好的代码,打成 zip 包发出去,那就是无损——解压出来 MD5 必须一致。
OTA 铁律:固件镜像、系统分区、校验文件、签名数据,一律使用无损压缩。有损压缩在 OTA 里唯一的合法用途,是多媒体资源包(开机动画、提示音)——而且必须确认播放器能容忍质量损失。
我曾经踩过一个坑:某次给 IoT 设备做差分升级,有人为了压得更小,对配置分区用了有损压缩。结果设备升级后 Wi-Fi 连不上——因为配置里的 CRC 校验值被压缩算法「优化」掉了。从那以后,我要求所有 OTA 镜像必须做解压后的完整性校验。
常见无损压缩算法简介
嵌入式 OTA 里,你大概率会跟下面这四位打交道。我按出场频率排个序:
| 算法 | 核心原理 | 压缩率 | 解压速度 | 内存占用 | 我推荐的使用场景 |
|---|---|---|---|---|---|
| gzip | DEFLATE + 简单头 | 中等(2~4x) | 极快 | 低(~32KB) | MCU、实时性要求高的场景 |
| zlib | DEFLATE + 流式接口 | 中等(2~4x) | 极快 | 低(~32KB) | 嵌入式流式解压、网络传输 |
| LZMA | LZ77 + 区间编码 | 高(3~6x) | 慢 | 高(~8MB+) | Linux 系统镜像、大文件离线压缩 |
| Brotli | LZ77 + Huffman + 上下文建模 | 很高(3~7x) | 中等 | 中等(~256KB) | Web 资源、现代 Linux 发行版 |
gzip —— 老兵不死
gzip 是我用得最多的算法。为什么?因为它快。在 Cortex-M4 上,解压速度能到 10MB/s 以上,内存只吃 32KB 的窗口缓冲区。
我记得有一次给一个只有 64KB RAM 的 MCU 做 OTA,别的算法根本跑不起来。最后选了 gzip 的硬解库,解压时逐块处理,完美适配。
小技巧:gzip 的压缩级别从 1 到 9。级别 1 压缩慢但压得小,级别 9 压缩快但压得大——等等,我是不是说反了?对,gzip 的级别是反直觉的:级别越高,压缩率越高,但压缩速度越慢。解压速度基本不受级别影响。所以 OTA 场景里,我通常用级别 6 做平衡。
zlib —— gzip 的孪生兄弟
zlib 和 gzip 底层都是 DEFLATE 算法。区别在于封装格式:
- gzip 有文件头尾,适合存成文件
- zlib 是流式接口,适合边收边解
你想想看,OTA 下载过程中,你不可能等整个包下载完再解压。那样内存会炸。所以流式解压才是王道。zlib 的 inflate() 函数,每次喂一块数据,吐一块解压结果,完美配合 HTTP 分块传输。
// 伪代码:zlib 流式解压
z_stream strm;
inflateInit(&strm);
while (有数据) {
strm.next_in = 下载缓冲区;
strm.avail_in = 收到的字节数;
inflate(&strm, Z_NO_FLUSH);
// 从 strm.next_out 取解压后的数据
// 写入 flash 或内存
}
inflateEnd(&strm);
LZMA —— 压缩率之王,但慢
LZMA 是 7-Zip 的看家本领。压缩率确实高,同样一个 4MB 的固件,gzip 能压到 1.5MB,LZMA 能压到 800KB。省下来的 700KB,在 NB-IoT 这种窄带网络上,可能就是少传 10 分钟。
但代价也很明显:解压慢,内存大。LZMA 的解压需要 8MB 以上的字典缓冲区。你让一个 STM32F103 跑 LZMA 解压?嗯,不是不行,但你要做好等 30 秒的心理准备。
避坑指南:我曾经在一个项目里用 LZMA 压缩了 16MB 的根文件系统。结果设备升级时,解压到一半内存溢出,直接看门狗复位。后来改成 gzip,虽然镜像大了 30%,但升级成功率从 60% 提升到了 99.5%。压缩率不是唯一指标,系统稳定性才是。
Brotli —— 后起之秀
Brotli 是 Google 搞出来的,最初为了压缩 Web 字体。后来发现它在固件压缩上也很能打。压缩率比 gzip 高 20%~30%,解压速度比 LZMA 快一个数量级。
我最近在几个 Linux 网关项目里开始用 Brotli。原因是:
- Linux 内核从 5.10 开始原生支持 Brotli 解压
- Brotli 的流式解压接口比 LZMA 友好得多
- 压缩率接近 LZMA,但解压速度是它的 5 倍
不过要注意,Brotli 在 MCU 上的移植还不成熟。我建议只在有 MMU、跑 Linux 的设备上用。
压缩率与解压速度的权衡
这是 OTA 压缩里最核心的决策点。没有银弹,只有取舍。
我画了一张图,帮你理解这个权衡关系:
你看这张图就明白了:
- gzip/zlib 在右下角,解压快,适合资源受限的 MCU
- LZMA 在左上角,压缩率最高,但解压慢、吃内存
- Brotli 在中间偏左,是个不错的折中方案
我个人选型时,会问自己三个问题:
- 设备 RAM 多大? 小于 64KB → 只能 gzip/zlib
- 升级包传输带宽多少? 窄带(< 50kbps)→ 优先考虑 LZMA 或 Brotli
- 允许升级耗时多久? 超过 5 分钟用户可能拔电 → 选解压快的算法
我的经验法则:如果你不确定选哪个,先上 gzip。它永远不会让你翻车。等性能分析出来,发现瓶颈在传输带宽而不是解压时间,再考虑换 Brotli 或 LZMA。别一开始就上最猛的算法——我见过太多项目因为「想压得更小」而把升级成功率搞崩了。