Android源码分析:LruCache 缓存机制实现原理

本文介绍了LRU缓存算法在Android中的应用,重点讲解了LruCache和DisLruCache的区别,以及LruCache如何通过LinkedHashMap实现LRU顺序。通过实例展示了LruCache的使用方法和核心原理,强调了系统化学习的重要性。
摘要由CSDN通过智能技术生成

因此LRU(Least Recently Used)缓存算法便应运而生,LRU是近期最少使用的算法,它的核心思想是当缓存满时,会优先淘汰那些近期最少使用的缓存对象,有效的避免了OOM的出现。在Android中采用LRU算法的常用缓存有两种:LruCacheDisLruCache,分别用于实现内存缓存和硬盘缓存,其核心思想都是LRU缓存算法。

其实LRU缓存的实现类似于一个特殊的栈,把访问过的元素放置到栈顶(若栈中存在,则更新至栈顶;若栈中不存在则直接入栈),然后如果栈中元素数量超过限定值,则删除栈底元素(即最近最少使用的元素)。详细算法实现如下图:

  1. 新数据压入到栈顶;
  2. 每当缓存命中(即缓存数据被访问),则将数据移到栈顶;
  3. 当栈满的时候,将栈底的数据丢弃。

举个例子演示一下:

二、LruCache的使用

LruCacheAndroid 3.1所提供的一个缓存类,所以在Android中可以直接使用LruCache实现内存缓存。而DisLruCache目前在Android 还不是Android SDK的一部分,但Android官方文档推荐使用该算法来实现硬盘缓存。

LruCache是个泛型类,主要算法原理是把最近使用的对象用强引用(即我们平常使用的对象引用方式)存储在 LinkedHashMap中。当缓存满时,把最近最少使用的对象从内存中移除,并提供了getput方法来完成缓存的获取和添加操作。

讲到LruCache不得不提一下LinkedHashMap,因为LruCache中Lru算法的实现就是通过LinkedHashMap来实现的。LinkedHashMap继承于HashMap,它使用了一个双向链表来存储Map中的Entry顺序关系,这种顺序有两种,一种是LRU顺序,一种是插入顺序,这可以由其构造函数public LinkedHashMap(int initialCapacity,float loadFactor, boolean accessOrder)的最后一个参数accessOrder来指定。所以,对于get、put、remove等操作,LinkedHashMap除了要做HashMap做的事情,还做些调整Entry顺序链表的工作。LruCache中将LinkedHashMap的顺序设置为LRU顺序来实现LRU缓存,每次调用get(也就是从内存缓存中取图片),则将该对象移到链表的尾端。调用put插入新的对象也是存储在链表尾端,这样当内存缓存达到设定的最大值时,将链表头部的对象(近期最少用到的)移除。

LruCache使用示例

LruCache的使用非常简单,我们就以图片缓存为例:

int maxMemory = (int) (Runtime.getRuntime().totalMemory()/1024);
int cacheSize = maxMemory/8;
mMemoryCache = new LruCache<String,Bitmap>(cacheSize){
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes()*value.getHeight()/1024;
}
};

  • 设置LruCache缓存的大小,一般为当前进程可用容量的1/8
  • 重写sizeOf方法,计算出要缓存的每张图片的大小。

注意:缓存的总容量和每个缓存对象的大小所用单位要一致。

LruCache的实现原理

LruCache的核心思想很好理解,就是要维护一个缓存对象列表,其中对象列表的排列方式是按照访问顺序实现的,即一直没访问的对象,将放在队尾,即将被淘汰。而最近访问的对象将放在队头,最后被淘汰。如下图所示:

那么这个队列到底是由谁来维护的,前面已经介绍了是由LinkedHashMap来维护。

LinkedHashMap是由数组+双向链表的数据结构来实现的。其中双向链表的结构可以实现访问顺序插入顺序,使得LinkedHashMap中的:

/**

  • Constructs a new {@code LinkedHashMap} instance with the specified
  • capacity, load factor and a flag specifying the ordering behavior.
  • @param initialCapacity
  •        the initial capacity of this hash map.
    
  • @param loadFactor
  •        the initial load factor.
    
  • @param accessOrder
  •        {@code true} if the ordering should be done based on the last
    
  •        access (from least-recently accessed to most-recently
    
  •        accessed), and {@code false} if the ordering should be the
    
  •        order in which the entries were inserted.
    

*/
public LinkedHashMap(
int initialCapacity, float loadFactor, boolean accessOrder) {
super(initialCapacity, loadFactor);
init();
this.accessOrder = accessOrder;
}

其中accessOrder设置为true则为访问顺序,为false,则为插入顺序

以具体例子解释,当设置为true时:

public static final void main(String[] args) {
LinkedHashMap<Integer, Integer> map = new LinkedHashMap<>(0, 0.75f, true);
map.put(0, 0);
map.put(1, 1);
map.put(2, 2);
map.put(3, 3);
map.put(4, 4);
map.put(5, 5);
map.put(6, 6);
map.get(1); //访问1
map.get(2); //访问2

for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + “:” + entry.getValue());
}
}

输出结果如下:

0:0
3:3
4:4
5:5
6:6
1:1
2:2

即最近访问的对象会被放到队尾,然后最后输出,那么这就正好满足的LRU缓存算法的思想。可见LruCache巧妙实现,就是利用了LinkedHashMap的这种数据结构。

下面我们在LruCache源码中具体看看,怎么应用LinkedHashMap来实现缓存的添加,获得和删除的。

LruCache源码分析

我们先看看成员变量有哪些:

public class LruCache<K, V> {
private final LinkedHashMap<K, V> map;

/** Size of this cache in units. Not necessarily the number of elements. */
private int size; //当前cache的大小
private int maxSize; //cache最大大小

private int putCount; //put的次数
private int createCount; //create的次数
private int evictionCount; //驱逐剔除的次数
private int hitCount; //命中的次数
private int missCount; //未命中次数

//…省略…
}

构造函数如下,可以看到LruCache正是用了LinkedHashMapaccessOrder=true构造参数实现LRU访问顺序:

public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException(“maxSize <= 0”);
}
this.maxSize = maxSize;
//将LinkedHashMap的accessOrder设置为true来实现LRU顺序
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}

put方法

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++; //插入次数加1
size += safeSizeOf(key, value); //更新缓存的大小
previous = map.put(key, value);
//如果已有缓存对象,则缓存大小的值需要剔除这个旧的大小
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}

//entryRemoved()是个空方法,可以自行实现
if (previous != null) {
entryRemoved(false, key, previous, value);
}

//调整缓存大小(关键方法)
trimToSize(maxSize);
return previous;
}

可以看到**put()方法并没有什么难点,重要的就是在添加过缓存对象后,调用trimToSize()**方法,来判断缓存是否已满,如果满了就要删除近期最少使用的算法。

trimToSize方法

public void trimToSize(int maxSize) {
while (true) {
K key;
V value;
synchronized (this) {
//如果map为空并且缓存size不等于0或者缓存size小于0,抛出异常
if (size < 0 || (map.isEmpty() && size != 0)) {
throw new IllegalStateException(getClass().getName()

  • “.sizeOf() is reporting inconsistent results!”);
    }

//如果缓存大小size小于最大缓存,或者map为空,则不需要再删除缓存对象,跳出循环
if (size <= maxSize || map.isEmpty()) {
break;
}

//迭代器获取第一个对象,即队头的元素,近期最少访问的元素
Map.Entry<K, V> toEvict = map.entrySet().iterator().next();

最后

我见过很多技术leader在面试的时候,遇到处于迷茫期的大龄程序员,比面试官年龄都大。这些人有一些共同特征:可能工作了5、6年,还是每天重复给业务部门写代码,工作内容的重复性比较高,没有什么技术含量的工作。问到这些人的职业规划时,他们也没有太多想法。

其实30岁到40岁是一个人职业发展的黄金阶段,一定要在业务范围内的扩张,技术广度和深度提升上有自己的计划,才有助于在职业发展上有持续的发展路径,而不至于停滞不前。

不断奔跑,你就知道学习的意义所在!

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 12
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值