4. 缓存系统基础

说到外部存储加载图片,很多人第一反应就是「慢」。确实,从SD卡或Flash读一张几百KB的图片,再解码成RGB565或ARGB8888,这个开销可不小。但LVGL不是傻傻地每次都重新读——它有一套缓存机制,专门解决这个问题。

我个人习惯把缓存比作「热手区」。你常用的工具放在手边,拿起来就快。LVGL的Cache也是这个道理:最近用过的图片数据,先留在内存里,下次再用就不用重新加载了。

4.1 为什么需要Cache?

先看一个真实场景。我在一个智能家居面板项目里,需要轮播显示6张天气图标。每张图标48×48像素,RGB565格式,算下来一张才4.5KB。但问题是,每次切换图标都要从外部Flash读取并解码。如果帧率是30fps,一秒就要读30次。你想想看,就算Flash读取速度再快,也扛不住这么频繁的操作。

更关键的是,解码过程本身就很吃CPU。LVGL的图片解码器需要把JPEG或PNG解成像素数据,这个计算量比单纯读Flash大得多。Cache的作用就是跳过这个重复的解码过程。

核心结论:Cache不是用来加速「读取」的,而是用来加速「复用」的。如果你的图片只用一次,Cache帮不了你;但如果同一张图片被反复显示,Cache能省掉90%以上的加载时间。

4.2 LVGL的Cache架构

LVGL的Cache机制其实不复杂。它维护了一个固定大小的内存池,专门存放解码后的图片数据。每次请求图片时,先查Cache——命中就直接返回,没命中才走完整的加载-解码流程。

我画了一张图,帮你理清这个流程:

LVGL图片加载与Cache流程 应用请求图片 检查Cache是否命中? 命中 直接返回缓存数据 未命中 从外部存储加载 解码图片数据 写入Cache并返回 返回图片数据给应用 💡 关键点 命中:零开销返回 未命中:加载+解码+缓存

这个流程里,最耗时的就是「加载」和「解码」两步。Cache命中的时候,这两步全跳过,速度自然快。

4.3 核心配置参数

LVGL的Cache配置集中在 lv_conf.h 里。我挑几个最关键的参数说:

参数名 默认值 说明
LV_IMG_CACHE_DEF_SIZE 0 Cache总大小(字节)。0表示关闭Cache
LV_IMG_CACHE_DEF_ENTRIES 0 最大缓存条目数。0表示不限制
LV_IMG_CACHE_MAX_ENTRY_SIZE 0 单张图片最大缓存大小。0表示不限制
⚠️ 注意:默认情况下,LV_IMG_CACHE_DEF_SIZE 是0,意味着Cache是关闭的。很多新手踩过这个坑——配置了半天外部存储,结果发现Cache根本没开。我曾经在一个项目里排查了整整两天,最后发现是忘了改这个宏。

4.4 缓存替换策略

Cache满了怎么办?LVGL用的是LRU(最近最少使用)策略。说白了,就是淘汰最久没被用过的那张图片。

这个策略在实际项目中表现怎么样?我举个例子。一个音乐播放器界面,有封面图、播放按钮、进度条背景。封面图每次切歌都会变,但按钮和背景是固定的。LRU策略下,按钮和背景图会一直留在Cache里,封面图不断被替换。这很合理——按钮和背景才是高频复用的资源。

但有一种情况要注意:如果你的界面是「轮播」模式,比如每隔5秒切换一张大图,而且图片都不重复,那LRU就没什么用。每张图只用一次就被踢出去了。这种情况下,我建议你考虑手动管理缓存,或者干脆把Cache关掉,省点内存。

4.5 手动控制缓存

除了自动的LRU,LVGL也提供了手动控制接口。我个人比较喜欢在特定场景下用这些API:

// 手动使图片进入缓存
lv_res_t res = lv_img_cache_invalidate_src(&my_img_dsc);
if (res == LV_RES_OK) {
    // 缓存失效成功,下次加载会重新读取
}

// 手动清空整个缓存
lv_img_cache_set_size(0);  // 先设为0
lv_img_cache_set_size(1024 * 50);  // 再重新设置,相当于清空

// 查询缓存状态
uint32_t cached_size = lv_img_cache_get_size();
uint32_t entry_count = lv_img_cache_get_entries();
💡 小技巧:在调试阶段,我经常在切换界面时手动清空缓存,确保不会因为缓存数据导致显示异常。上线前再把这个调试代码去掉。

4.6 缓存命中率的计算

怎么知道你的Cache设置得好不好?看命中率。LVGL没有直接提供命中率统计,但你可以自己加:

static uint32_t cache_hit_count = 0;
static uint32_t cache_miss_count = 0;

// 在图片加载回调里统计
lv_res_t my_img_decoder_open(lv_img_decoder_t *decoder, lv_img_decoder_dsc_t *dsc) {
    if (lv_img_cache_is_cached(dsc->src)) {
        cache_hit_count++;
        // 命中,直接返回
        return LV_RES_OK;
    }
    cache_miss_count++;
    // 未命中,走正常加载流程
    // ...
}

// 计算命中率
float hit_rate = (float)cache_hit_count / (cache_hit_count + cache_miss_count);

我一般要求项目里的缓存命中率不低于70%。低于这个值,说明Cache配置不合理,要么太小,要么图片复用率太低。

4.7 内存碎片问题初探

这里埋个伏笔——Cache用久了会产生内存碎片。因为图片大小不一,频繁分配释放会导致内存空洞。LVGL的Cache本身不处理碎片,它只负责LRU淘汰。碎片问题怎么解决?我们会在后面的章节专门讲。

但你现在可以记住一个原则:尽量让缓存图片的大小一致。比如所有图标都缩放到48×48,所有封面都缩放到200×200。这样分配的内存块大小固定,碎片就少很多。

本章要点回顾:
  • Cache的核心作用是跳过重复的解码过程,不是加速读取
  • 默认Cache是关闭的,记得在 lv_conf.h 里打开
  • LRU策略适合高频复用的场景,轮播场景效果有限
  • 手动控制缓存API在调试和特殊场景下很有用
  • 命中率低于70%时,需要重新评估Cache配置
  • 图片大小一致可以减少内存碎片