Android LruCache 源码深度解析:缓存策略、最佳实践与性能优化
在 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 作为二级缓存,用于持久化缓存数据。这种多级缓存架构可以有效地提升应用的性能和可靠性。
相关阅读
更多推荐

所有评论(0)