LVGL 图片缓存机制解析
前言
本文主要说明 LVGL 图片缓存的机制,不适用于将图片编码进程序的做法。这里提供如何修改该机制以便适配一些场景的思路。v8
版本的缓存策略比较简单我们可以适当修改策略。在v9
版本缓存策略使用了lru策略进行管理,缓存机制比较完善了解即可。
v8 版本图片绘制流程
绘制图片的起点函数是lv_draw_img
,我们分析这个函数的流程即可。其流程图如下:
看起来有点复杂,其实流程是很清晰的,我们一步步分析,这里仅以软件绘制的流程进行分析。
首先要知道的是,绘制函数的回调函数是在初始化屏幕lv_disp_drv_init
的时候就已经把绘制的回调函数注册好了。
看图就可以知道,调用lv_draw_img
这个流程往下走就是绘制图像,软件绘制是不会使用draw_img
这个指针,因此会走下一步,解码和绘制。在decode_and_draw 这个函数会打开缓存和进行绘制的操作。绘制失败则 show error。
基本的绘制流程已经清晰了,下面将会了解图片缓存机制,实际就是了解 _lv_img_cache_open 这个实现流程
v8 版本 图片缓存机制
图片缓存机制看图已经很清晰了,这里就简要描述一下,不过多赘述了。其缓存机制就是,每一次取缓存的时候,减少缓存的生命周期;如果找到了某个缓存,则其生命周期会设为解码图片时间*权重;否则找到生命周期最少的缓存,释放该解码器和内存,然后解码图片,设置其生命周期。
这个缓存机制简单而好用,对于频繁使用的图片会进行缓存,不常使用的就会被释放。可以适用于以下场景,有一个背景图是不会改变的,可以将其缓存图片张数设置大一点,这样,绘制图片的时候,由于背景比较常用,就不会被释放。
魔改 v8 版本缓存机制
尽管 LVGL v8 的缓存机制适用于大多数场景,但在某些特定情况下需要对其进行定制以满足特定的需求。例如,在内存有限的情况下,假设有大量图片需要处理,但只有两张图片是高频使用的。此时的目标是如何确保这两张高频使用的图片始终保留在缓存中,而其他图片则可以随用随解码并显示。
这里的核心思路就是:在每次调用 _lv_img_cache_open 函数时,将这两张高频使用的图片的生命周期设置为最大值。这样可以确保这两张图片不会因为缓存空间不足而被替换掉。其他图片则按照正常的缓存机制进行管理,即按需解码并在显示后从缓存中移除。
实现参考
下面是在 lv_img_cache.h 添加的接口和定义
/**
* @file lv_img_cache.h
*
*/
#ifndef LV_IMG_CACHE_H
#define LV_IMG_CACHE_H
#ifdef __cplusplus
extern "C" {
#endif
/*********************
* INCLUDES
*********************/
#include "lv_img_decoder.h"
/*********************
* DEFINES
*********************/
/**********************
* TYPEDEFS
**********************/
/**
* When loading images from the network it can take a long time to download and decode the image.
*
* To avoid repeating this heavy load images can be cached.
*/
typedef struct {
lv_img_decoder_dsc_t dec_dsc; /**< Image information*/
/** Count the cache entries's life. Add `time_to_open` to `life` when the entry is used.
* Decrement all lifes by one every in every ::lv_img_cache_open.
* If life == 0 the entry can be reused*/
int32_t life;
#if LV_IMG_CACHE_CUSTOM
const void * not_release_src;
uint16_t not_release;
#endif
} _lv_img_cache_entry_t;
/**********************
* GLOBAL PROTOTYPES
**********************/
/**
* Open an image using the image decoder interface and cache it.
* The image will be left open meaning if the image decoder open callback allocated memory then it will remain.
* The image is closed if a new image is opened and the new image takes its place in the cache.
* @param src source of the image. Path to file or pointer to an `lv_img_dsc_t` variable
* @param color The color of the image with `LV_IMG_CF_ALPHA_...`
* @param frame_id the index of the frame. Used only with animated images, set 0 for normal images
* @return pointer to the cache entry or NULL if can open the image
*/
_lv_img_cache_entry_t * _lv_img_cache_open(const void * src, lv_color_t color, int32_t frame_id);
/**
* Set the number of images to be cached.
* More cached images mean more opened image at same time which might mean more memory usage.
* E.g. if 20 PNG or JPG images are open in the RAM they consume memory while opened in the cache.
* @param new_entry_cnt number of image to cache
*/
void lv_img_cache_set_size(uint16_t new_slot_num);
/**
* Invalidate an image source in the cache.
* Useful if the image source is updated therefore it needs to be cached again.
* @param src an image source path to a file or pointer to an `lv_img_dsc_t` variable.
*/
void lv_img_cache_invalidate_src(const void * src);
/**********************
* MACROS
**********************/
#if LV_IMG_CACHE_CUSTOM
void lv_img_cache_mark_not_auto_release_src(const void * src);
#endif
#ifdef __cplusplus
} /*extern "C"*/
#endif
#endif /*LV_IMG_CACHE_H*/
下面是在 lv_img_cache.c 添加的实现
/**
* @file lv_img_cache.c
*
*/
/*********************
* INCLUDES
*********************/
#include "../misc/lv_assert.h"
#include "lv_img_cache.h"
#include "lv_img_decoder.h"
#include "lv_draw_img.h"
#include "../hal/lv_hal_tick.h"
#include "../misc/lv_gc.h"
/*********************
* DEFINES
*********************/
/*Decrement life with this value on every open*/
#define LV_IMG_CACHE_AGING 1
/*Boost life by this factor (multiply time_to_open with this value)*/
#define LV_IMG_CACHE_LIFE_GAIN 1
/*Don't let life to be greater than this limit because it would require a lot of time to
* "die" from very high values*/
#define LV_IMG_CACHE_LIFE_LIMIT 1000
/**********************
* TYPEDEFS
**********************/
/**********************
* STATIC PROTOTYPES
**********************/
#if LV_IMG_CACHE_DEF_SIZE
static bool lv_img_cache_match(const void * src1, const void * src2);
#endif
/**********************
* STATIC VARIABLES
**********************/
#if LV_IMG_CACHE_DEF_SIZE
static uint16_t entry_cnt;
#if LV_IMG_CACHE_CUSTOM
static bool *img_cache_custom_processed;
#endif
#endif
/**********************
* MACROS
**********************/
/**********************
* GLOBAL FUNCTIONS
**********************/
/**
* Open an image using the image decoder interface and cache it.
* The image will be left open meaning if the image decoder open callback allocated memory then it will remain.
* The image is closed if a new image is opened and the new image takes its place in the cache.
* @param src source of the image. Path to file or pointer to an `lv_img_dsc_t` variable
* @param color color The color of the image with `LV_IMG_CF_ALPHA_...`
* @return pointer to the cache entry or NULL if can open the image
*/
_lv_img_cache_entry_t * _lv_img_cache_open(const void * src, lv_color_t color, int32_t frame_id)
{
/*Is the image cached?*/
_lv_img_cache_entry_t * cached_src = NULL;
#if LV_IMG_CACHE_DEF_SIZE
if(entry_cnt == 0) {
LV_LOG_WARN("lv_img_cache_open: the cache size is 0");
return NULL;
}
_lv_img_cache_entry_t * cache = LV_GC_ROOT(_lv_img_cache_array);
/*Decrement all lifes. Make the entries older*/
uint16_t i;
for(i = 0; i < entry_cnt; i++) {
if(cache[i].life > INT32_MIN + LV_IMG_CACHE_AGING) {
cache[i].life -= LV_IMG_CACHE_AGING;
}
}
#if LV_IMG_CACHE_CUSTOM
uint16_t j;
lv_memset_00(img_cache_custom_processed, sizeof(bool) * entry_cnt);
for(i = 0; i < entry_cnt; i++) {
if(cache[i].not_release || cache[i].not_release_src == NULL || img_cache_custom_processed[i])
continue;
img_cache_custom_processed[i] = true;
for (j = i; j < entry_cnt; j++) {
if(lv_img_cache_match(cache[i].not_release_src, cache[j].dec_dsc.src)) {
cache[i].life = LV_IMG_CACHE_LIFE_LIMIT;
img_cache_custom_processed[j] = true;
continue;;
}
}
}
#endif
for(i = 0; i < entry_cnt; i++) {
if(color.full == cache[i].dec_dsc.color.full &&
frame_id == cache[i].dec_dsc.frame_id &&
lv_img_cache_match(src, cache[i].dec_dsc.src)) {
/*If opened increment its life.
*Image difficult to open should live longer to keep avoid frequent their recaching.
*Therefore increase `life` with `time_to_open`*/
cached_src = &cache[i];
cached_src->life += cached_src->dec_dsc.time_to_open * LV_IMG_CACHE_LIFE_GAIN;
if(cached_src->life > LV_IMG_CACHE_LIFE_LIMIT) cached_src->life = LV_IMG_CACHE_LIFE_LIMIT;
LV_LOG_TRACE("image source found in the cache");
break;
}
}
/*The image is not cached then cache it now*/
if(cached_src) return cached_src;
cached_src = &LV_GC_ROOT(_lv_img_cache_single);
/*Open the image and measure the time to open*/
uint32_t t_start = lv_tick_get();
lv_res_t open_res = lv_img_decoder_open(&cached_src->dec_dsc, src, color, frame_id);
if(open_res == LV_RES_INV) {
LV_LOG_WARN("Image draw cannot open the image resource");
lv_memset_00(cached_src, sizeof(_lv_img_cache_entry_t));
cached_src->life = INT32_MIN; /*Make the empty entry very "weak" to force its us*/
return NULL;
}
cached_src->life = 0;
/*If `time_to_open` was not set in the open function set it here*/
if(cached_src->dec_dsc.time_to_open == 0) {
cached_src->dec_dsc.time_to_open = lv_tick_elaps(t_start);
}
if(cached_src->dec_dsc.time_to_open == 0) cached_src->dec_dsc.time_to_open = 1;
#if LV_IMG_CACHE_CUSTOM
for(i = 0; i < entry_cnt; i++) {
if (!cache[i].not_release)
continue;
if (lv_img_cache_match(cache[i].not_release_src, cached_src->dec_dsc.src)) {
cache[i].not_release = 1;
break;
}
}
#endif
return cached_src;
}
/**
* Set the number of images to be cached.
* More cached images mean more opened image at same time which might mean more memory usage.
* E.g. if 20 PNG or JPG images are open in the RAM they consume memory while opened in the cache.
* @param new_entry_cnt number of image to cache
*/
void lv_img_cache_set_size(uint16_t new_entry_cnt)
{
#if LV_IMG_CACHE_DEF_SIZE == 0
LV_UNUSED(new_entry_cnt);
LV_LOG_WARN("Can't change cache size because it's disabled by LV_IMG_CACHE_DEF_SIZE = 0");
#else
if(LV_GC_ROOT(_lv_img_cache_array) != NULL) {
/*Clean the cache before free it*/
lv_img_cache_invalidate_src(NULL);
lv_mem_free(LV_GC_ROOT(_lv_img_cache_array));
}
/*Reallocate the cache*/
LV_GC_ROOT(_lv_img_cache_array) = lv_mem_alloc(sizeof(_lv_img_cache_entry_t) * new_entry_cnt);
LV_ASSERT_MALLOC(LV_GC_ROOT(_lv_img_cache_array));
if(LV_GC_ROOT(_lv_img_cache_array) == NULL) {
entry_cnt = 0;
return;
}
entry_cnt = new_entry_cnt;
#if LV_IMG_CACHE_CUSTOM
if (img_cache_custom_processed)
lv_mem_free(img_cache_custom_processed);
img_cache_custom_processed = lv_mem_alloc(sizeof(bool) * entry_cnt);
#endif
/*Clean the cache*/
lv_memset_00(LV_GC_ROOT(_lv_img_cache_array), entry_cnt * sizeof(_lv_img_cache_entry_t));
#endif
}
/**
* Invalidate an image source in the cache.
* Useful if the image source is updated therefore it needs to be cached again.
* @param src an image source path to a file or pointer to an `lv_img_dsc_t` variable.
*/
void lv_img_cache_invalidate_src(const void * src)
{
LV_UNUSED(src);
#if LV_IMG_CACHE_DEF_SIZE
_lv_img_cache_entry_t * cache = LV_GC_ROOT(_lv_img_cache_array);
uint16_t i;
for(i = 0; i < entry_cnt; i++) {
if(src == NULL || lv_img_cache_match(src, cache[i].dec_dsc.src)) {
if(cache[i].dec_dsc.src != NULL) {
lv_img_decoder_close(&cache[i].dec_dsc);
}
lv_memset_00(&cache[i], sizeof(_lv_img_cache_entry_t));
}
}
#endif
}
#if LV_IMG_CACHE_CUSTOM
void lv_img_cache_mark_not_auto_release_src(const void * src)
{
uint16_t i;
_lv_img_cache_entry_t * cache = LV_GC_ROOT(_lv_img_cache_array);
for(i = 0; i < entry_cnt; i++) {
if (lv_img_cache_match(cache[i].not_release_src, src))
return;
}
for(i = 0; i < entry_cnt; i++) {
if (cache[i].not_release_src == NULL) {
cache[i].not_release_src = src;
cache[i].not_release = 0;
return;
}
}
}
#endif
/**********************
* STATIC FUNCTIONS
**********************/
#if LV_IMG_CACHE_DEF_SIZE
static bool lv_img_cache_match(const void * src1, const void * src2)
{
lv_img_src_t src_type = lv_img_src_get_type(src1);
if(src_type == LV_IMG_SRC_VARIABLE)
return src1 == src2;
if(src_type != LV_IMG_SRC_FILE)
return false;
if(lv_img_src_get_type(src2) != LV_IMG_SRC_FILE)
return false;
return strcmp(src1, src2) == 0;
}
#endif
接口调用例子
接口调用仅添加了lv_img_cache_mark_not_auto_release_src
其他接口功能一致。
lv_img_cache_mark_not_auto_release_src(path);// 标记不自动释放
lv_img_cache_invalidate_src(path); // 把改不自动释放的路径删除
lv_img_cache_invalidate_src(NULL); // 清空所有缓存以及不自动释放的缓存
额外 - - v9 缓存机制简要解析
v9 解码显示流程
通过观察这个图,发现软件绘制的流程跟lvgl v8的流程是类似的,其绘制流程如下:
- 打开解码器:无论是 LVGL v8 还是 v9,在开始处理图像之前,都会首先打开相应的解码器。
- 缓存检查:随后,系统会检查缓存中是否已有解码后的图像数据。如果存在,则直接使用缓存中的数据;如果没有,则解码图像并将结果添加到缓存中。
- 执行绘制:接着,执行实际的绘制操作,将图像显示在屏幕上。
- 关闭解码器:最后,当绘制完成后,关闭解码器,并可能清理相关的缓存条目。
LV\GL v8 与 v9 的差异:
- LVGL v8 中,PNG、JPEG、BMP 等格式的图像共享同一套缓存机制。这意味着无论图像格式如何,缓存管理逻辑是一致的。
- LVGL v9 则采取了一种更为灵活的方法,其中每个图像格式(如 PNG、JPEG、BMP 等)可以有自己独立的缓存和解码管理机制。这允许开发者针对不同的图像类型定制缓存策略,以优化特定场景下的性能。
v9 缓存机制
缓存机制
- LVGL v9 版本中,默认采用了 LRU(Least Recently Used,最近最少使用)缓存淘汰策略。这一策略确保了最近最少使用的缓存项会被优先替换,以维持缓存中的内容为最近最常访问的数据。
- 底层实现上,该策略结合了红黑树和双向链表的数据结构来高效地维护缓存项的访问顺序及存储位置。
缓存操作流程
- 缓存和解码流程 主要通过 lv_img_decode_open 函数完成,该函数负责图像的解码和缓存管理。
- 当需要从缓存中获取数据时,lv_cache_acquire 函数会查找并返回最近使用过的缓存项。同时,该函数还会更新缓存项的位置,将其移动到链表头部,以此标记为最新使用。
- 在缓存空间不足时,系统会调用一系列函数来释放最近最少使用的缓存项,包括但不限于:
decoder_open
lv_image_decoder_add_to_cache
lv_cache_add
cache_add_internal_no_lock
cache_evict_one_internal_no_lock
这些函数共同协作,确保缓存空间得到有效利用,同时释放不再需要的缓存项以腾出空间。
v8 和 v9缓存机制的对比
共性:
- 两者都能有效满足大多数使用场景,并能够缓存最常用的图像资源。
差异:
- LVGL v9 提供了更为精细的控制选项,允许开发者设置图像缓存的最大大小,从而有效地防止内存溢出问题。此外,v9 还提供了自定义缓存管理接口,使用户能够根据特定需求实施定制化的缓存策略。
- LVGL v8 则不具备上述特性,对于缓存管理功能的增强需要通过直接修改源代码的方式来实现。
总结
- 两个版本均能满足基本的应用需求,但 LVGL v9 在缓存控制方面提供了更高的灵活性与安全性,相较于 LVGL v8 更加优越。
了解更多内容请看: