在 Android 应用开发中,图片加载、网络请求结果、甚至是复杂的计算结果,频繁地从磁盘或网络获取会严重影响用户体验。为了避免重复计算和 I/O 操作,缓存机制至关重要。android.util.LruCache 提供了一个基于 Least Recently Used (LRU) 算法的内存缓存实现,可以有效地提升应用的性能和响应速度。LruCache 的核心设计思想在于用有限的内存空间,存储近期最常访问的数据,当缓存满时,淘汰最近最少使用的数据,从而达到最佳的缓存命中率。

例如,在图片加载框架 Glide 或 Picasso 中,LruCache 通常被用作一级缓存,位于内存中,速度最快。相比之下,磁盘缓存速度较慢,但容量更大,可作为二级缓存。这种多级缓存架构在大型应用中非常常见。

LruCache 的优势与局限性

LruCache 的主要优势在于简单易用、性能较高。它直接集成在 Android SDK 中,无需引入额外的依赖库。此外,LRU 算法在大多数场景下都能取得不错的缓存效果。然而,LruCache 也有一些局限性:

  • 容量限制: LruCache 的容量必须预先设定,且一旦设定后难以动态调整。这意味着开发者需要根据应用的实际情况和内存使用情况,谨慎地选择合适的缓存大小。如果缓存过小,会导致频繁的缓存淘汰,降低缓存命中率;如果缓存过大,可能会导致内存溢出。
  • 线程安全: LruCache 是线程安全的,但并发访问仍然可能导致性能瓶颈。在高并发场景下,可以考虑使用更高级的并发缓存策略,例如 ConcurrentHashMap 或 Guava Cache。
  • 无法持久化: LruCache 仅用于内存缓存,数据不会持久化到磁盘。如果应用进程被杀死,缓存数据会丢失。因此,在需要持久化缓存的场景下,需要结合磁盘缓存或其他持久化方案。

Android LruCache 源码剖析

要充分利用 LruCache,深入理解其源码至关重要。下面我们将从构造函数、put() 方法、get() 方法、trimToSize() 方法等方面,详细剖析 LruCache 的实现原理。理解 Android LruCache 的源码对理解 Android 内存管理也有一定的帮助。

构造函数

LruCache 的构造函数接受一个 maxSize 参数,用于指定缓存的最大容量。这个容量单位可以是任意的,例如图片的像素数量、字节数,甚至是对象的个数。关键在于 sizeOf() 方法需要返回相同单位的值。以下是 LruCache 的构造函数:

public LruCache(int maxSize) {    if (maxSize <= 0) {        throw new IllegalArgumentException("maxSize <= 0");    }    this.maxSize = maxSize; // 设置缓存的最大容量    this.map = new LinkedHashMap<K, V>(0, 0.75f, true); // 初始化 LinkedHashMap}

这里 LinkedHashMap 是 LruCache 的核心数据结构,它维护了一个双向链表,用于记录缓存数据的访问顺序。构造函数的第三个参数 accessOrder 设置为 true,表示按照访问顺序排序,最近访问的数据会被移动到链表的末尾。

put() 方法

put() 方法用于向缓存中添加数据。如果 key 已经存在,则更新 value;如果 key 不存在,则添加新的 key-value 对。在添加数据后,trimToSize() 方法会被调用,用于检查缓存是否超出最大容量,如果超出则进行淘汰。

public final V put(K key, V value) {    if (key == null || value == null) {        throw new NullPointerException("key == null || value == null");    }    V previous;    synchronized (this) {        putCount  ;        size  = safeSizeOf(key, value); // 更新缓存大小        previous = map.put(key, value); // 将 key-value 对添加到 LinkedHashMap 中        if (previous != null) {            size -= safeSizeOf(key, previous); // 如果 key 已经存在,则更新缓存大小        }    }    if (previous != null) {        entryRemoved(false, key, previous, value); // 回调 entryRemoved() 方法    }    trimToSize(maxSize); // 检查缓存是否超出最大容量    return previous;}

get() 方法

get() 方法用于从缓存中获取数据。如果 key 存在,则将该 key-value 对移动到链表的末尾,并返回 value;如果 key 不存在,则返回 null。

public final V get(K key) {    if (key == null) {        throw new NullPointerException("key == null");    }    synchronized (this) {        V mapValue = map.get(key); // 从 LinkedHashMap 中获取 value        if (mapValue != null) {            hitCount  ;            return mapValue;        }        missCount  ;    }    return null;}

trimToSize() 方法

trimToSize() 方法是 LruCache 的核心方法,它负责淘汰最近最少使用的数据,直到缓存大小小于等于最大容量。淘汰策略基于 LinkedHashMap 的访问顺序,最久未被访问的数据会被优先淘汰。

private 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 || map.isEmpty()) {                break; // 缓存大小小于等于最大容量,或者缓存为空,则退出循环            }            Map.Entry<K, V> toEvict = map.entrySet().iterator().next(); // 获取链表的第一个元素,即最近最少使用的数据            if (toEvict == null) {                break; // 缓存为空,则退出循环            }            key = toEvict.getKey();            value = toEvict.getValue();            map.remove(key); // 从 LinkedHashMap 中移除 key-value 对            size -= safeSizeOf(key, value); // 更新缓存大小            evictionCount  ;        }        entryRemoved(true, key, value, null); // 回调 entryRemoved() 方法    }}

LruCache 的实战技巧与避坑指南

在使用 LruCache 时,需要注意以下几点:

选择合适的缓存大小

缓存大小的选择需要根据应用的实际情况进行权衡。如果缓存过小,会导致频繁的缓存淘汰,降低缓存命中率;如果缓存过大,可能会导致内存溢出。建议使用 Android Profiler 等工具,监控应用的内存使用情况,并根据实际情况调整缓存大小。可以考虑使用总内存的1/8作为缓存大小。

重写 sizeOf() 方法

sizeOf() 方法用于计算单个缓存对象的大小。默认情况下,sizeOf() 方法返回 1,这意味着缓存容量的单位是对象的个数。如果缓存对象的大小不一致,或者缓存容量需要以字节数或其他单位来衡量,则需要重写 sizeOf() 方法。例如,在缓存图片时,可以根据图片的像素数量来计算缓存大小。

注意线程安全问题

LruCache 是线程安全的,但并发访问仍然可能导致性能瓶颈。在高并发场景下,可以考虑使用更高级的并发缓存策略,例如 ConcurrentHashMap 或 Guava Cache。

谨慎处理 entryRemoved() 回调

entryRemoved() 方法在缓存对象被移除时被调用。开发者可以在 entryRemoved() 方法中执行一些清理工作,例如释放资源。需要注意的是,entryRemoved() 方法在同步块中被调用,因此不应该执行耗时操作,否则会阻塞其他线程。如果需要执行耗时操作,可以将其放入后台线程中执行。

例如,在使用 LruCache 缓存 Bitmap 对象时,可以在 entryRemoved() 方法中调用 bitmap.recycle() 方法,释放 Bitmap 对象占用的内存。这样做可以有效地避免内存泄漏。

考虑使用 DiskLruCache 作为二级缓存

LruCache 仅用于内存缓存,数据不会持久化到磁盘。如果应用进程被杀死,缓存数据会丢失。因此,在需要持久化缓存的场景下,可以结合 DiskLruCache 作为二级缓存。DiskLruCache 可以将数据缓存到磁盘上,即使应用进程被杀死,缓存数据仍然存在。

在实际项目中,可以结合 LruCache 和 DiskLruCache,构建一个多级缓存系统。LruCache 作为一级缓存,用于缓存最近最常访问的数据;DiskLruCache 作为二级缓存,用于持久化缓存数据。这种多级缓存架构可以有效地提升应用的性能和可靠性。

相关阅读

Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐