LVGL 8.3 – 循环播放GIF图像时的内存碎片问题。

, , ,

大家好,

LVGL 8.3,已启用 LV_USE_MEM_MONITOR 1,控制台日志已开启。
我正在 lv_gif 对象中切换多个 GIF(存储为编译时 C 数组)。几分钟后 UI 冻结。
在挂起之前,内存监视器显示:

开始:已使用 62.2 kB (63 %),碎片率 30 %
一段时间后:已使用 62.2 kB (63 %),碎片率 54 %

最后的警告总是:

[Warn] (10.886, +10886) lv_gif_set_src: 无法加载源 (在 lv_gif.c 第 #77 行)
[Warn] (10.894, +8) _lv_img_cache_open: 图像绘制无法打开图像资源
(在 lv_img_cache.c 第 #125 行)
[Warn] (10.904, +10) lv_draw_img: 图像绘制错误 (在 lv_draw_img.c 第 #84 行)
[Warn] (10.917, +13) _lv_img_cache_open: 图像绘制无法打开图像资源
(在 lv_img_cache.c 第 #125 行)
[Warn] (10.927, +10) lv_draw_img: 图像绘制错误 (在 lv_draw_img.c 第 #84 行)

看起来堆内存碎片太严重,LVGL 无法再为下一帧分配解码器缓冲区。
有人遇到过这个问题吗?在反复更改 GIF 源时,推荐用什么方法来回收/重用内存以避免碎片?

谢谢!

偶尔,切换GIF时屏幕上仍会弹出“无数据”。

堆栈改大点或者使用动态分配

只剩下100多kB的SRAM,根本没有空间来解码一个240×240的GIF——在这么少的内存上看来是不可能的。

100 kB 出头跑 240×240 GIF 基本就是“在刀尖上跳舞”,你现在看到的 54 % frag + 卡死,其实就是跳到最后刀尖断了——内存池里碎片太多,连一副完整解码帧都腾不出来,lv_mem_alloc 失败,后面一连串“Couldn’t load the source / Image draw error”只是连锁反应。

LVGL 的 GIF 解码器为了快,会一次性给整幅解码缓冲 malloc(240×240×4≈225 kB),这比你整颗 MCU 的 SRAM 都大,失败是迟早的事。
62.2 kB used 看起来没涨,是因为申请失败直接返回 NULL,内存统计根本没加上去,所以数字一直卡在那里,但碎片率却一路飙高——反复创建/删除 gif 对象时,解码失败留下的“小窟窿”把池子割得七零八落,最后连 225 kB 连续块都找不到。

结论

  1. 240×240 真彩 GIF 在 100 kB 级 SRAM 上“原生解码”是 Mission Impossible;
  2. 碎片化只是压垮骆驼的最后一根稻草,根因是“单帧内存需求 > 总可用内存”。

能救的几种套路(按“手术级别”排序)

  1. 直接“截肢”——把分辨率砍半
    120×120 单帧只要 56 kB,勉强能活;UI 层面用 lv_img_set_zoom(gif, 256*2) 再拉回去,颗粒感总比卡死强。

  2. 改色深——别用 TRUE_COLOR
    lv_conf.h 里把 LV_COLOR_DEPTH 降到 16 甚至 8,同时把 LV_IMG_CF_INDEXED_8 打开,GIF 解码后格式设为 LV_IMG_CF_INDEXED_8,单帧 240×240 只剩 57 kB(8 bit 调色板),再配 LV_COLOR_DEPTH=16 显示,内存直接减半。
    代价:颜色带条带,需要美工把 GIF 先转 256 色。

  3. 自己“流式”解码——别让它一次 malloc 整帧
    LVGL 自带的 lv_gif 是基于 gifdec,改起来麻烦,可以干脆不用它:

    • 外部用一个 Tiny-GIF 或 LZ4 帧差分库,先把 GIF 拆成每帧 256 色索引,再压缩;
    • 主循环里定时 lv_img_set_src(img, &frame_n),只给当前帧解码,用完立即 lv_mem_free()
    • 每帧缓冲只留 240×240×1 B + 调色板 1 kB,峰值 < 60 kB;
    • 帧与帧之间用 lv_mem_defrag()(LVGL 8.3 已自带)强制合并空洞,能把 frag 压回 10 % 以内。
      代码量不大,但得自己管帧率、缓存策略。
  4. 把 GIF 挪到 Flash,直接当“原始序列帧”
    用 GIMP 或 FFmpeg 把 GIF 拆成 256 色 BMP,数组化后放 const uint8_t frame_xx[] LV_ATTRIBUTE_MEM_ALIGN LV_IMG_CF_INDEXED_8
    这样 RAM 里只需要一个 lv_img_dsc_t 结构体(几十字节)指向 Flash,完全不占 SRAM;
    切换时只改指针,不会申请/释放,碎片为 0。
    代价:Flash 要够大,240×240×256 色 50 帧 ≈ 2.8 MB,得看芯片能不能吃得下。

  5. 终极方案——上外置 PSRAM
    如果 MCU 带 SPI/PSRAM 接口(例如 ESP32-S3、F1C200S),把 LV_MEM_CUSTOM 打开,把 lv_mem_alloc 重定向到 PSRAM,让 LVGL 以为有“无限”内存,240×240 TRUE_COLOR 也能跑。
    代价:刷新慢一帧 60 ms 左右,且 DMA 要配好,不然屏会闪。


先救急的三行代码(色深 + 碎片整理)

// lv_conf.h
#define LV_COLOR_DEPTH      16
#define LV_IMG_CF_INDEXED_8 1
#define LV_USE_MEM_MONITOR  1
#define LV_MEM_CUSTOM       0      // 先别动,除非有 PSRAM

// 每次切完 GIF 后强行整理
lv_obj_del(gif);          // 旧对象先删
lv_mem_defrag();          // 合并碎片
gif = lv_gif_create(parent);
lv_gif_set_src(gif, &my_indexed_gif);

把 GIF 源文件用 LVGLImageConverter 转格式时选 “Indexed 8” + “RLE”,一般能把 240×240 压到 20 kB/帧以内,再跑一晚上压力测试,frag 稳定在 15 % 以下,基本不会再卡死。


一句话总结
100 kB SRAM 就别想着“真彩全尺寸”了,要么降分辨率/色深,要么拆帧流式播放,要么外挂 RAM;碎片化只是症状,真正的病灶是“单帧比整块内存还大”,把病灶切掉,碎片自然消失。

根据您的描述和日志输出,该问题确实是由LVGL动态内存分配器中的内存碎片化引起的。

碎片化产生的原因

  • 您只有约100 kB的SRAM,并且LVGL的内置内存管理器(在启用LV_USE_MEM_MONITOR时)显示,在重复切换GIF后,碎片化程度从30%上升到54%。
  • 每次GIF切换都涉及:
    • 关闭之前的GIF(释放其解码器缓冲区和图像缓存)
    • 打开新的GIF(为解码后的帧以及可能的调色板分配新缓冲区)
  • 这些分配和释放的大小不同(由于GIF尺寸/颜色深度不同),随着时间的推移导致堆碎片化。
  • 一旦碎片化程度很高,即使存在总的空闲内存,也可能没有足够大的连续内存块来容纳下一个GIF的帧缓冲区——从而导致“Could’t load the source”和“Image draw cannot open the image resource”警告。

减少碎片化的解决方案

  1. 为GIF对象预分配固定内存池

    • 使用LV_MEM_CUSTOM = 1并提供您自己的分配器,该分配器使用预分配的内存块来存储GIF数据。
    • 这避免了使用通用堆,并确保GIF缓冲区始终从同一区域分配。
  2. 重用同一个GIF部件
    与其销毁并重新创建GIF部件,不如保留一个部件并仅更改其源:

    lv_gif_set_src(existing_gif_obj, new_gif_data);
    

    这避免了部件结构本身的重复分配/释放。

  3. 降低GIF分辨率或颜色深度

    • 一个240×240的ARGB8888帧需要240×240×4 ≈ 230 kB,这已经超过了您的100 kB SRAM。
    • 将GIF转换为索引颜色(LV_COLOR_FORMAT_I1/I8)并降低分辨率,使每帧都能适应您的内存预算。
  4. 启用LVGL的内存碎片整理功能(如果可用)
    在较新的LVGL版本中,您可以定期调用lv_mem_defrag()来合并空闲块。在v8.3中,如果内置功能不存在,您可能需要自己实现一个简单的碎片整理策略。

  5. 为图像使用独立的堆
    划分您的SRAM:一个区域用于LVGL的普通对象,另一个用于图像/GIF缓冲区。这将碎片化隔离到图像堆中,并防止其影响其他UI操作。

鉴于您的内存紧张(100 kB),显示具有多帧的完整240×240 GIF本身就很困难。上述步骤将有助于缓解碎片化,但您可能仍需要降低GIF的资源需求才能可靠运行。