3、图片解码机制:解码流程详解、解码器(ImageDecoder)的工作原理、解码过程中的内存峰值
好,咱们进入第三章。前两章我们把图片在 TouchGFX 里的存储形态和颜色格式聊透了。这一章,我带你看看图片从「躺在 Flash 里」到「显示在屏幕上」中间到底发生了什么。
说白了,就是解码。你想想看,一张 JPEG 或者 PNG,压缩得再小,屏幕也认不了。屏幕只认像素点阵——每个点什么颜色,你得给我摆好。那谁来干这个转换的活?就是 ImageDecoder。
3.1 解码流程:一张图是怎么「上屏」的?
我习惯把解码流程拆成四个阶段。每个阶段都有它的内存特征,尤其是峰值,后面我会重点讲。
- 文件读取阶段:TouchGFX 从外部 Flash 或文件系统里,把图片的原始压缩数据读出来。这时候内存里存的还是 JPEG/PNG 的二进制流。
- 解码器初始化:ImageDecoder 根据图片格式,创建对应的解码上下文。比如 JPEG 需要初始化 Huffman 表、量化表;PNG 需要初始化 zlib 流。
- 逐行/逐块解码:解码器开始干活。JPEG 是 MCU(最小编码单元)一块一块解;PNG 是逐行解压再过滤。每解出一块,就往目标缓冲区里写。
- 像素格式转换:解码出来的原始数据通常是 RGB888 或 RGBA8888,但 TouchGFX 的 framebuffer 可能是 RGB565 或 L8。这一步要做颜色空间转换和抖动处理。
核心要点:解码不是一次性把整张图全解完再显示。它是边解边画,或者边解边往缓存里填。这个「边解边用」的机制,直接决定了你的内存峰值。
我在一个项目里遇到过这样的情况:一张 800x480 的 JPEG 背景图,解码时内存直接飙了 300KB 多。当时我以为是图片太大,后来一查,是解码器的中间缓冲区没复用。嗯,这里要注意。
3.2 ImageDecoder 的工作原理
TouchGFX 的 ImageDecoder 其实是一个抽象层。它不自己解图片,而是调用底层的解码库。对于 JPEG,它用 libjpeg-turbo;对于 PNG,它用 libpng。你可以在 TouchGFXConfiguration.cpp 里看到它的初始化逻辑。
它的工作模式,我总结成一张图:
从图上你能看到,解码器不是直接把压缩数据变成像素。它需要几个中间缓冲区来周转。这些缓冲区的大小,就是内存峰值的来源。
个人经验:我调试过一个 PNG 解码卡顿的问题。后来发现是 ImageDecoder 每次解码一行都要重新分配缓冲区。改成预分配后,内存峰值降了 40%。所以,预分配 vs 按需分配,在嵌入式里是天壤之别。
3.3 解码过程中的内存峰值
好,重点来了。内存峰值到底怎么算?我直接给你公式:
内存峰值 = 解码输入缓冲区 + 中间行缓冲区 × N + 目标缓冲区
其中:
- 解码输入缓冲区:存放从 Flash 读出来的压缩数据块。JPEG 通常一次读 4KB~16KB;PNG 一次读一个 IDAT 块,可能更大。
- 中间行缓冲区:这是大头。JPEG 解码需要 8 行或 16 行的 YCbCr 缓冲区(取决于采样因子)。PNG 需要 1 行原始像素缓冲区 + 1 行过滤后的缓冲区。
- 目标缓冲区:最终像素要写到哪里。如果是直接写到 framebuffer,那这部分不算额外开销。如果是写到动态位图,那就要算上整张图的大小。
我举个例子,一张 800x480 的 JPEG,颜色格式 RGB565:
| 缓冲区 | 大小计算 | 典型值 |
|---|---|---|
| 解码输入缓冲区 | 4KB ~ 16KB | 8 KB |
| 中间行缓冲区 (JPEG 8行) | 800 × 8 × 3 (YCbCr) = 19.2 KB | 20 KB |
| 目标缓冲区 (动态位图) | 800 × 480 × 2 (RGB565) = 768 KB | 768 KB |
| 峰值总计 | 8 + 20 + 768 | 796 KB |
注意:如果目标缓冲区是 framebuffer,那 768KB 可能已经在显存里了,不占用额外 RAM。但如果是动态位图,这 768KB 就是实打实的堆内存开销。我曾经在一个项目里就是因为没区分这个,导致内存溢出,系统反复重启。
那怎么降峰值?我分享三个实战技巧:
- 用 framebuffer 做目标:如果图片是直接显示,不要先解到动态位图再拷贝。直接解到 framebuffer 的对应区域。
- 缩小中间行缓冲区:JPEG 解码时,如果图片是 4:2:0 采样,中间行缓冲区可以只保留 8 行。不要用默认的 16 行。
- 流式解码:TouchGFX 支持边解码边显示。这样目标缓冲区只需要一行的空间,而不是整张图。代价是 CPU 负载会高一些。
避坑指南:我曾经在 STM32F429 上做过一个相框项目。用了 JPEG 动态解码,内存峰值算下来 800KB,而芯片总共才 1MB RAM。后来我把解码改成流式,峰值降到 120KB,系统稳如狗。所以,峰值不是固定的,是设计出来的。
3.4 不同图片格式的解码内存对比
最后,我整理了一张对比表。你选图片格式时,可以直接参考:
| 格式 | 解码方式 | 中间缓冲区 | 峰值特征 | 推荐场景 |
|---|---|---|---|---|
| JPEG | MCU 逐块 | 8~16 行 YCbCr | 中等,与行数相关 | 照片、渐变背景 |
| PNG | 逐行解压 | 2 行 RGBA | 较低,但解压慢 | 图标、透明图 |
| BMP | 无压缩 | 无 | 极低,但存储大 | 小尺寸、调试用 |
| GIF | LZW 逐帧 | 1 帧 + 调色板 | 中等,帧动画叠加 | 简单动画 |