3、图片解码机制:解码流程详解、解码器(ImageDecoder)的工作原理、解码过程中的内存峰值

好,咱们进入第三章。前两章我们把图片在 TouchGFX 里的存储形态和颜色格式聊透了。这一章,我带你看看图片从「躺在 Flash 里」到「显示在屏幕上」中间到底发生了什么。

说白了,就是解码。你想想看,一张 JPEG 或者 PNG,压缩得再小,屏幕也认不了。屏幕只认像素点阵——每个点什么颜色,你得给我摆好。那谁来干这个转换的活?就是 ImageDecoder。

3.1 解码流程:一张图是怎么「上屏」的?

我习惯把解码流程拆成四个阶段。每个阶段都有它的内存特征,尤其是峰值,后面我会重点讲。

  1. 文件读取阶段:TouchGFX 从外部 Flash 或文件系统里,把图片的原始压缩数据读出来。这时候内存里存的还是 JPEG/PNG 的二进制流。
  2. 解码器初始化:ImageDecoder 根据图片格式,创建对应的解码上下文。比如 JPEG 需要初始化 Huffman 表、量化表;PNG 需要初始化 zlib 流。
  3. 逐行/逐块解码:解码器开始干活。JPEG 是 MCU(最小编码单元)一块一块解;PNG 是逐行解压再过滤。每解出一块,就往目标缓冲区里写。
  4. 像素格式转换:解码出来的原始数据通常是 RGB888 或 RGBA8888,但 TouchGFX 的 framebuffer 可能是 RGB565 或 L8。这一步要做颜色空间转换和抖动处理。

核心要点:解码不是一次性把整张图全解完再显示。它是边解边画,或者边解边往缓存里填。这个「边解边用」的机制,直接决定了你的内存峰值。

我在一个项目里遇到过这样的情况:一张 800x480 的 JPEG 背景图,解码时内存直接飙了 300KB 多。当时我以为是图片太大,后来一查,是解码器的中间缓冲区没复用。嗯,这里要注意。

3.2 ImageDecoder 的工作原理

TouchGFX 的 ImageDecoder 其实是一个抽象层。它不自己解图片,而是调用底层的解码库。对于 JPEG,它用 libjpeg-turbo;对于 PNG,它用 libpng。你可以在 TouchGFXConfiguration.cpp 里看到它的初始化逻辑。

它的工作模式,我总结成一张图:

ImageDecoder 解码流程与内存峰值节点 ImageDecoder 解码流程与内存峰值节点 原始压缩数据 Flash / 文件系统 解码输入缓冲区 存放压缩数据流 ImageDecoder 核心 JPEG: MCU 逐块解码 PNG: 逐行解压+过滤 中间行缓冲区 JPEG: 8/16 行;PNG: 1 行 像素格式转换 RGB888 → RGB565 / L8 目标缓冲区 Framebuffer / 动态位图 ⚠ 内存峰值区域 输入缓冲区 + 中间行缓冲区 + 目标缓冲区同时存在

从图上你能看到,解码器不是直接把压缩数据变成像素。它需要几个中间缓冲区来周转。这些缓冲区的大小,就是内存峰值的来源。

个人经验:我调试过一个 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 就是实打实的堆内存开销。我曾经在一个项目里就是因为没区分这个,导致内存溢出,系统反复重启。

那怎么降峰值?我分享三个实战技巧:

  1. 用 framebuffer 做目标:如果图片是直接显示,不要先解到动态位图再拷贝。直接解到 framebuffer 的对应区域。
  2. 缩小中间行缓冲区:JPEG 解码时,如果图片是 4:2:0 采样,中间行缓冲区可以只保留 8 行。不要用默认的 16 行。
  3. 流式解码:TouchGFX 支持边解码边显示。这样目标缓冲区只需要一行的空间,而不是整张图。代价是 CPU 负载会高一些。

避坑指南:我曾经在 STM32F429 上做过一个相框项目。用了 JPEG 动态解码,内存峰值算下来 800KB,而芯片总共才 1MB RAM。后来我把解码改成流式,峰值降到 120KB,系统稳如狗。所以,峰值不是固定的,是设计出来的。

3.4 不同图片格式的解码内存对比

最后,我整理了一张对比表。你选图片格式时,可以直接参考:

格式 解码方式 中间缓冲区 峰值特征 推荐场景
JPEG MCU 逐块 8~16 行 YCbCr 中等,与行数相关 照片、渐变背景
PNG 逐行解压 2 行 RGBA 较低,但解压慢 图标、透明图
BMP 无压缩 无 极低,但存储大 小尺寸、调试用
GIF LZW 逐帧 1 帧 + 调色板 中等,帧动画叠加 简单动画