如何进行图片缓存

本文详细分析了Android中图片缓存的重要性,探讨了LRU和LFU两种缓存算法的工作原理,以及它们在实际应用中的优缺点。同时,文章还介绍了Android系统自带的LruCache,Glide的缓存机制,以及OkHttp的DiskLruCache,帮助读者深入理解图片缓存的实现细节。

大厂面试问“图片如何进行缓存”的时候主要在考察什么?

  • 是否对缓存淘汰算有一定的研究;
  • 是否对常见的图片加载框架有深入研究;
  • 是否对算法效果有验证闭环的意识,对使用场景是否适合。

一、题目剖析

1.1 如何对图片进行缓存?

图片加载过程

  • 缓存介质,网络/磁盘/内存缓存;
  • 缓存算法的设计分析(关键,不同的目标对象、使用场景,使用不同的算法的效果是有差别的);
  • 以熟悉的框架为例分析它的缓存机制;
  • 要有验证算法效果的(优化、量化)意识。
    缓存算法

1.2 如何评价缓存算法是否合适?

  • 获取成本很高的话缓存就很值;
  • 缓存对象很大,1g,内存都不够用,缓存不合适;
  • 缓存的价值是由获取成本、缓存成本和时间决定的,很有价值的缓存对象,之后用的不多了或者不再用了,它的缓存价值就趋于零了。缓存算法的衡量指标,用一个词来描述就是“命中率”。
    在这里插入图片描述

二、缓存算法

缓存算法

缓存算法要解决的问题:缓存媒介放满后,新来的缓存媒介里没有对象,要放在什么位置?把哪个对象淘汰?真实算法设计中还要考虑每个对象的权重。算法就决定谁被干掉。

2.1 LRU(Least Recently Used,最近最少使用)

依次访问A-B-D-D,由于D已经缓存过了,因此当再次访问D的时候直接用缓存里得对象:
LRU
再次对B访问时,B再次命中后,依然直接用缓存对象,但是要移动B到缓存末尾:
在这里插入图片描述
访问C缓存没有,缓存未满,直接插到缓存末尾,保持最近使用的一直排在最后面:
在这里插入图片描述
这时再访问A,缓存有,直接拿来用,并将A移至末尾:
在这里插入图片描述
接着继续访问E,缓存中没有缓存过,需要放进缓存,此时介质存满了(达到上限),需要删掉一个原有对象,LRU就要把缓存起始位置的D干掉,E依然放到最后位置:
在这里插入图片描述

2.2 LFU(Least Frequently Used,最不经常使用)

从使用频率的角度来讲还有LFU,需要看具体使用场景,大多数的图片都是用LRU算法。
依次访问A-B-D-D,D访问两次,会对D对象计数为2:
在这里插入图片描述
继续接着访问B,要把B计数为2,并移到末尾处:
在这里插入图片描述
继续访问C,由于第一次对C访问,需要加入缓存:
在这里插入图片描述
继续访问A,把缓存原有的A计数加一,为2,移至末尾位置:
在这里插入图片描述
接着,再访问E,缓存内没有,需要放至缓存,恰巧缓存也没有多余的位置,需要删除一个原有的对象,LFU会干掉最近没再使用C,并把E放进去。其实,使用频率相同的情况,也有排序,最近使用过的在最后。
在这里插入图片描述
LRU能清楚算法设计的细节就很OK,LFU辅助理解。

三、实现细节看代码

3.1 Android里面就有LruCache

/* android.util. LruCache */

// 泛型是key和value,key是查找时的标识,value就是缓存对象,如bitmap
public class LruCache<K, V> {
    // 所有lru的实现里面都有一个内部map数据结构,注意构造参数
    @UnsupportedAppUsage
    private final LinkedHashMap<K, V> map;

    private int size;
    private int maxSize;

	// 统计监控用的变量,看似没用,额外占用了4x5=20个字节,主要用来监控算法的有效性
	// 一点额外的开销,可以监控到整个运行状态,是否需要继续调整算法全部取决于这些监控数据
    private int putCount;//存放个数
    private int createCount;// 创建个数
    private int evictionCount;// 弹出个数
    private int hitCount;// 命中个数
    private int missCount;// 未命中个数

    public LruCache(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        this.maxSize = maxSize;
        // 初始化的时候new出来,true表示每次访问后会的对象被丢到整个链表数据结构末尾
        this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
    }
    // …
    protected int sizeOf(K key, V value) {
        return 1;// 权重,默认1
    }
	// …
    public final V get(K key) {
        if (key == null) {
            throw new NullPointerException("key == null");
        }

        V mapValue;
    	// 取的时候加锁范围尽可能要小,不要对整个类或者方法枷锁
        synchronized (this) {
            mapValue = map.get(key);
            if (mapValue != null) {
                hitCount++;
                return mapValue;
            }
            missCount++;
        }
    	// create通常需要复写实现的,所以没加锁
    	// 创建不需要涉及lru内部数据的访问,不需要加锁
        V createdValue = create(key);
        if (createdValue == null) {
            return null;
        }
        // 如果没使用CAS和volatile,加锁是最简单的线程安全
        synchronized (this) {
            createCount++;
            mapValue = map.put(key, createdValue);

            if (mapValue != null) {
                // There was a conflict so undo that last put
                map.put(key, mapValue);
            } else {
                size += safeSizeOf(key, createdValue);
            }
        }

        if (mapValue != null) {
            entryRemoved(false, key, createdValue, mapValue);
            return mapValue;
        } else {
            trimToSize(maxSize);
            return createdValue;
        }
    }
	// …
	// 新进来对象超出了限制个数,就会弹出现有的对象
    public void trimToSize(int maxSize) {
        while (true) {
            K key;
            V value;
            synchronized (this) {
                if (size < 0 || (map.isEmpty() && size != 0)) {
                    throw new IllegalStateException(getClass().getName()
                            + ".sizeOf() is reporting inconsistent results!");
                }

                if (size <= maxSize) {
                    break;
                }

                Map.Entry<K, V> toEvict = map.eldest();
                if (toEvict == null) {
                    break;
                }

                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key);
                size -= safeSizeOf(key, value);
                evictionCount++;
            }

            entryRemoved(true, key, value, null);
        }
    }
	// …
}

3.2 Glide也有类的cache类

没有create

/* com.bumptech.glide.util.LruCache */

public class LruCache<T, Y> {
	private final Map<T, Entry<Y>> cache = new LinkedHashMap<>(100, 0.75f, true);
	private final long initialMaxSize;
	private long maxSize;
	private long currentSize;

	@Nullable
	public synchronized Y put(@NonNull T key, @Nullable Y item) {
	  final int itemSize = getSize(item);
	  if (itemSize >= maxSize) {
	    onItemEvicted(key, item);
	    return null;
	  }
	
	  if (item != null) {
	    currentSize += itemSize;
	  }
	  @Nullable Entry<Y> old = cache.put(key, item == null ? null : new Entry<>(item, itemSize));
	  if (old != null) {
	    currentSize -= old.size;
	
	    if (!old.value.equals(item)) {
	      onItemEvicted(key, old.value);
	    }
	  }
	  evict();
	
	  return old != null ? old.value : null;
	}
	// …
	protected synchronized void trimToSize(long size) {
	  Map.Entry<T, Entry<Y>> last;
	  Iterator<Map.Entry<T, Entry<Y>>> cacheIterator;
	  while (currentSize > size) {
	    // 取元素
	    cacheIterator = cache.entrySet().iterator();
	    last = cacheIterator.next();
	    final Entry<Y> toRemove = last.getValue();
	    currentSize -= toRemove.size;
	    final T key = last.getKey();
	    cacheIterator.remove();
	    onItemEvicted(key, toRemove.value);
	  }
}

3.3 OkHttp的DiskLruCache

/* okhttp3.internal.cache.DiskLruCache */

public final class DiskLruCache implements Closeable, Flushable {
final LinkedHashMap<String, Entry> lruEntries = new LinkedHashMap<>(0, 0.75f, true);

void trimToSize() throws IOException {
  while (size > maxSize) {
    Entry toEvict = lruEntries.values().iterator().next();
    removeEntry(toEvict);
  }
  mostRecentTrimFailed = false;
}

3.4 LinkedHashMap源码

// ...
public V get(Object key) {
    Node<K,V> e;
    if ((e = getNode(hash(key), key)) == null)
        return null;
    if (accessOrder)
        afterNodeAccess(e);// move node to last
    return e.value;
}
/*
* 将被访问的元素移到链表尾部,迭代器访问的第一个元素最老,通常就是要移除的对象
* 其实只要看那一行注释就明了了
*/
void afterNodeAccess(Node<K,V> e) { // move node to last
    LinkedHashMapEntry<K,V> last;
    if (accessOrder && (last = tail) != e) {
        LinkedHashMapEntry<K,V> p =
            (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
        p.after = null;
        if (b == null)
            head = a;
        else
            b.after = a;
        if (a != null)
            a.before = b;
        else
            last = b;
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
        tail = p;
        ++modCount;
    }
}

四、总结

  • 探讨缓存的形式、价值衡量标准
  • 阐述LRU、LFU算法的设计思路
  • 分析常见框架的LRU算法实现

参考

大厂资深面试官 带你破解Android高级面试

<think>好的,我现在要帮助用户解决在LVGL中使用解码器从U盘加载图片时如何实现缓存机制的问题。首先,我需要回忆一下LVGL的缓存机制相关知识。记得LVGL的图片解码器支持缓存,可以避免重复解码,特别是对于从较慢的存储介质如U盘中加载图片的情况。 用户的问题涉及到U盘图片加载和缓存机制的实现方式。我需要分步骤思考如何实现。首先,应该确认用户是否已经正确初始化了文件系统,能够访问U盘中的图片文件。这部分可能不是用户的主要问题,但可能需要提及。 接下来是缓存机制的设置。LVGL的图片缓存有两种类型:全局缓存和局部缓存。全局缓存作用于所有图片,而局部缓存针对特定图片。用户可能需要配置全局缓存的大小,这可以通过`lv_img_cache_set_size(size)`函数来设置。例如,设置缓存条目数量为100,这样常用图片的解码数据会被保留,减少重复解码的开销。 然后是解码器的配置。用户需要确保图片解码器已经正确注册,并且支持缓存的选项被启用。比如,在打开图片时使用`LV_IMG_FLAG_CACHEABLE`标志,这样解码后的数据会被存入缓存。同时,需要注意缓存的失效时间,可能需要调整`lv_conf.h`中的相关参数,如`LV_IMG_CACHE_DEF_SIZE`和`LV_IMG_CACHE_DEF_TIME`,以适应U盘较慢的读取速度。 此外,用户可能需要处理大图片的情况。对于较大的图片,如果内存有限,可以考虑使用局部缓存或者在不需要时手动清理缓存条目。例如,调用`lv_img_cache_invalidate_src(src)`来移除特定图片缓存,或者使用`LV_IMG_CF_TRUE_COLOR_ALPHA`格式来直接存储解码后的数据,减少后续解码次数。 还需要考虑异步加载的问题。如果图片加载导致界面卡顿,可以结合文件系统的异步读取功能,或者使用LVGL的任务系统分步加载图片。例如,先加载缩略图,再在后台加载完整图片,提升用户体验。 最后,用户可能需要优化文件系统访问速度。比如,使用较大的读取缓冲区,或者预先缓存U盘的文件列表,减少频繁的文件访问操作。这些步骤都能提升图片加载的效率。 总结一下,实现LVGL的U盘图片缓存需要配置全局或局部缓存,正确设置解码器参数,处理大图片的内存管理,并结合异步加载和文件系统优化。需要根据具体应用场景调整缓存大小和失效时间,确保性能和内存使用的平衡。</think>### LVGL U盘图片加载缓存机制实现步骤 #### 1. 文件系统初始化 首先确保正确挂载U盘文件系统,使用LVGL的`lv_fs_drv_t`注册文件系统驱动: ```c lv_fs_drv_t fs_drv; lv_fs_drv_init(&fs_drv); fs_drv.letter = 'U'; // 指定盘符 fs_drv.ready_cb = uart_ready_cb; // 硬件就绪检测 fs_drv.open_cb = uart_open_cb; // 文件打开回调 lv_fs_drv_register(&fs_drv); ``` #### 2. 全局缓存设置 通过`lv_img_cache_set_size()`设置缓存条目数量: ```c lv_img_cache_set_size(100); // 缓存最近使用的100张图片 ``` 这会自动缓存所有带有`LV_IMG_FLAG_CACHEABLE`标志的图片解码结果[^1]。 #### 3. 解码器配置 在图片打开时添加缓存标志: ```c lv_img_set_src(img, "U:/images/photo.jpg?cache"); // 自动添加LV_IMG_FLAG_CACHEABLE ``` 或显式指定: ```c lv_img_set_src(img, &((lv_img_dsc_t){ .data = "U:/images/photo.jpg", .header.cf = LV_IMG_CF_RAW, .header.flags = LV_IMG_FLAG_CACHEABLE })); ``` #### 4. 缓存失效控制 修改`lv_conf.h`调整缓存策略: ```c #define LV_IMG_CACHE_DEF_SIZE 100 // 默认缓存条目 #define LV_IMG_CACHE_DEF_TIME 1000 // 缓存保留时间(ms) #define LV_IMG_CACHE_DEF_GC_TIME 30000 // 垃圾回收间隔 ``` #### 5. 大图片处理 对于超过缓存容量的图片: ```c // 手动控制缓存 lv_img_cache_entry_t * entry = lv_img_cache_open(src, LV_COLOR_WHITE); if(entry) { // 使用entry->decoded lv_img_cache_close(entry); // 减少引用计数 } // 强制清除特定缓存 lv_img_cache_invalidate_src(src); ``` #### 6. 异步加载实现 结合FreeRTOS队列实现后台解码: ```c xTaskCreate(img_loader_task, "img_loader", 2048, NULL, 1, NULL); void img_loader_task(void *pv) { while(1) { char *path = xQueueReceive(img_queue, portMAX_DELAY); lv_img_decoder_dsc_t dsc; lv_img_decoder_open(&dsc, path, LV_COLOR_WHITE); // 解码完成后发送事件通知UI更新 } } ``` #### 优化建议 - 使用`lv_fs_read`时设置512字节以上的缓冲区 - 对缩略图使用`LV_IMG_CF_TRUE_COLOR`格式预解码 - 监控缓存命中率: ```c LV_IMG_CACHE_HIT_NUM // 命中次数 LV_IMG_CACHE_MISS_NUM // 未命中次数 ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小山研磨代码

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值