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: 無法載入來源 (in lv_gif.c line #77)
[Warn] (10.894, +8) _lv_img_cache_open: 影像繪製無法開啟影像資源
(in lv_img_cache.c line #125)
[Warn] (10.904, +10) lv_draw_img: 影像繪製錯誤 (in lv_draw_img.c line #84)
[Warn] (10.917, +13) _lv_img_cache_open: 影像繪製無法開啟影像資源
(in lv_img_cache.c line #125)
[Warn] (10.927, +10) lv_draw_img: 影像繪製錯誤 (in lv_draw_img.c line #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 的幀緩衝區——進而導致「Couldn’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 的資源需求才能穩定運作。