3、图片解码流程:LVGL图片解码器架构、解码器注册与优先级、解码回调函数详解
图片解码,说白了就是把硬盘里的 JPEG、PNG 文件,变成 LVGL 能直接拿来刷屏的像素数据。这个过程如果出了问题,你看到的要么是花屏,要么是黑块,要么直接卡死。
我刚开始用 LVGL 做外部存储加载时,就栽在这上面。明明文件读出来了,但显示就是不对。后来才发现,是解码器的注册顺序搞错了。嗯,这节课我们就把它彻底讲透。
3.1 LVGL 图片解码器架构
LVGL 的解码器架构,其实是一个典型的「策略模式」。它不关心你的图片是存在 Flash、SD 卡还是网络,它只关心你有没有注册对应的解码器。
整个流程可以概括为三步:
- 文件读取:通过文件系统接口拿到原始字节流
- 格式识别:遍历已注册的解码器,找到能处理这个格式的那个
- 像素转换:把压缩数据解压成 LVGL 的
lv_img_dsc_t结构
下面这张图,是我个人习惯用来跟团队新人讲架构的,你一看就明白:
核心要点:LVGL 不直接解码图片。它把解码工作委托给注册好的解码器。你只需要告诉它「这个格式谁来解」,剩下的它自己搞定。
3.2 解码器注册与优先级
解码器不是自动生效的。你得手动注册。而且注册顺序决定了谁先被调用。
LVGL 内部维护了一个解码器链表。每次有新图片要解码,它就从头到尾遍历这个链表,挨个问「你能处理这个文件吗?」。第一个说「能」的,就接手干活。
注册函数就一个:
lv_res_t lv_img_decoder_add(lv_img_decoder_t * decoder);
你想想看,如果你先注册了一个「万能解码器」,它什么格式都说能解,那后面的专用解码器就永远没机会了。我在项目中就遇到过这种坑——自己写了个 BMP 解码器,结果 PNG 解码器先注册了,它看到 BMP 文件头不认识,直接返回失败,但我的 BMP 解码器排在后面,根本没被问到。
所以,优先级规则其实很简单:
| 注册顺序 | 优先级 | 说明 |
|---|---|---|
| 先注册 | 高 | 先被遍历,先被询问 |
| 后注册 | 低 | 只有前面的都拒绝,才轮到它 |
个人建议:把专用解码器(比如 PNG、JPEG)注册在前面,通用或自定义解码器注册在后面。这样专用解码器能快速命中,性能更好。
3.3 解码回调函数详解
每个解码器需要实现两个回调:info_cb 和 open_cb。还有一个可选的 close_cb。
这三个回调,构成了解码器的完整生命周期。
3.3.1 info_cb:探路先锋
这个回调的作用很简单——看一眼文件头,判断这个格式我能不能解。能,返回 LV_RES_OK;不能,返回 LV_RES_INV。
原型长这样:
lv_res_t (*info_cb)(struct _lv_img_decoder_t * decoder, const void * src, lv_img_header_t * header);
注意第三个参数 header。如果你确定能解这个文件,需要在这里填上图片的宽、高、格式等信息。LVGL 会拿这些信息去分配内存。
我曾经犯过一个低级错误:在 info_cb 里只返回了 OK,忘了填 header。结果 LVGL 分配了 0 字节的缓冲区,后面一写就崩。嗯,这种问题最难查。
3.3.2 open_cb:真正干活的
这个回调负责把图片数据解压成像素。原型:
lv_res_t (*open_cb)(struct _lv_img_decoder_t * decoder, struct _lv_img_decoder_dsc_t * dsc);
这里有个关键结构体 lv_img_decoder_dsc_t,它包含了:
- src:图片源,可能是文件路径、指针或符号
- header:图片信息(info_cb 里填的那些)
- img_data:解码后的像素数据,你要把结果填到这里
- time_to_open:解码耗时,用于性能统计
实际解码时,你通常要做这几步:
- 从
dsc->src读取原始数据 - 调用对应的解码库(比如 libjpeg、lodepng)
- 把解压后的 RGB 数据写入
dsc->img_data - 设置
dsc->header.cf为正确的色彩格式
注意:img_data 指向的内存由 LVGL 管理,你只管写,不要自己 malloc。否则内存泄漏等着你。
3.3.3 close_cb:打扫战场
这个回调是可选的。如果你在 open_cb 里申请了额外资源(比如临时缓冲区、文件句柄),就在这里释放。
原型:
void (*close_cb)(struct _lv_img_decoder_t * decoder, struct _lv_img_decoder_dsc_t * dsc);
大部分情况下,你不需要实现它。但如果你用了外部解码库,比如 libjpeg 的 jpeg_create_decompress,那一定要在 close_cb 里调用 jpeg_destroy_decompress。否则每解码一张图,内存就涨一块。
3.4 一个完整的自定义解码器示例
说了这么多,不如直接看代码。下面是一个最简单的 BMP 解码器骨架:
static lv_res_t decoder_info(lv_img_decoder_t * decoder, const void * src, lv_img_header_t * header)
{
/* 检查文件头是否为 "BM" */
const uint8_t * data = (const uint8_t *)src;
if (data[0] != 'B' || data[1] != 'M') return LV_RES_INV;
/* 填写图片信息 */
header->w = *(uint16_t *)&data[18];
header->h = *(uint16_t *)&data[22];
header->cf = LV_IMG_CF_TRUE_COLOR;
return LV_RES_OK;
}
static lv_res_t decoder_open(lv_img_decoder_t * decoder, lv_img_decoder_dsc_t * dsc)
{
/* 这里做真正的解码 */
/* 从 dsc->src 读取像素数据 */
/* 写入 dsc->img_data */
return LV_RES_OK;
}
void my_decoder_init(void)
{
lv_img_decoder_t * dec = lv_mem_alloc(sizeof(lv_img_decoder_t));
lv_img_decoder_init(dec);
dec->info_cb = decoder_info;
dec->open_cb = decoder_open;
lv_img_decoder_add(dec);
}
避坑指南:我曾经在 decoder_info 里直接读取文件系统,结果导致 LVGL 在绘制图片列表时频繁打开文件,性能暴跌。正确的做法是:info_cb 只做格式识别,不要做任何 I/O 操作。I/O 留给 open_cb。
3.5 调试日志分级方法
解码出问题时,怎么定位?我习惯把日志分成三级:
| 级别 | 宏定义 | 输出内容 |
|---|---|---|
| 错误 | LV_LOG_ERROR |
解码失败、内存不足、文件不存在 |
| 警告 | LV_LOG_WARN |
格式不匹配、降级处理、性能异常 |
| 信息 | LV_LOG_INFO |
解码器注册成功、图片尺寸、耗时 |
在 open_cb 里,我通常会加一行:
LV_LOG_INFO("Decode image: %dx%d, format: %d",
dsc->header.w, dsc->header.h, dsc->header.cf);
这样一旦花屏,我就能从日志里看到「解码器到底输出了什么尺寸和格式」,跟实际显示一对比,问题就清楚了。
嗯,解码器这块内容不少,但核心就三点:架构是策略模式、注册顺序决定优先级、回调函数各司其职。把这三点吃透了,外部存储加载图片的底子就算打牢了。