谈谈LruCache算法的底层实现原理及其内部源码

我们在对数据进行操作的时候,为了避免流量或者性能的消耗,我们对于一些数据都会进行缓存处理,而对数据的缓存的要点不仅仅只有我们所熟悉的存储缓存和使用缓存,还有删除缓存。对于添加和获取缓存很好理解,那么为什么还要对缓存进行删除呐?原因很简单,因为我们的手机容量是有限的,如果我们拼命的写入缓存,那么终有一天内存会满导致程序奔溃,这显然不是我们想要的结果,于是我们在写入缓存的时候并非无脑写入,而是在写入之前会对内存空间进行一次判断,如果内存不足了,那么就移除掉一些缓存数据,从而达到内存不会爆掉的状态。

LruCache算法

根据上面所说的思想,android系统内部帮我们写了一个API——LruCache,来帮我们处理一些缓存。

LruCache(Least Recently Used)算法的核心思想就是最近最少使用算法。他在算法的内部维护了一个LinkHashMap的链表,通过put数据的时候判断是否内存已经满了,如果满了,则将最近最少使用的数据给剔除掉,从而达到内存不会爆满的状态。这么讲可能有些抽象,我从网上找了一张图来解释这个算法。


20180820102252299.png

通过上面这张图,我们可以看到,LruCache算法内部其实是一个队列的形式在存储数据,先进来的数据放在队列的尾部,后进来的数据放在队列头部,如果要使用队列中的数据,那么使得之后将其又放在队列的头部,如果要存储数据并且发现数据已经满了,那么便将队列尾部的数据给剔除掉,从而达到我们使用缓存的目的。这里需要注意一点,队尾存储的数据就是我们最近最少使用的数据,也就是我们在内存满的时候需要剔除掉的数据。

这里有一个疑问,为什么LruCache内部原理的实现需要用到LinkHashMap来存储数据呐?

因为LinkHashMap内部是一个数组加双向链表的形式来存储数据,他能够保证插入时候的数据和取出来的时候数据的顺序的一致性。也就是说,我们以什么样的顺序插入数据,就会以什么样的顺序取出数据。并且更重要的一点是,当我们通过get方法获取数据的时候,这个获取的数据会从队列中跑到队列头来,从而很好的满足我们LruCache的算法设计思想。

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);
map.get(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

可以看到,我们在插入的时候是0,1,2,3,4,5,6的形式插入的,然后调用get方法将1,2取出来。最后遍历LinkHashMap,发现最先出来的数据变成了2和1,其余的数据都在这两个数据之后取出来,这就达到了我们想要 的结果和目的。

LruCache算法的使用

Bitmap bitmap = null;
//获取运行内存大小的八分之一
int memory = (int)Runtime.getRuntime().totalMemory() / 1024;
int cache = memory / 8;
bitmap = BitmapFactory.decodeResource(getResources(),R.mipmap.ic_launcher);
LruCache<String,Bitmap> lruCache = new LruCache<String,Bitmap>(cache){
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
}
};
//将数据存储进入缓存
lruCache.put("cacheBitmap",bitmap);

    Bitmap cacheBitmap = lruCache.get("cacheBitmap");
    //在使用的时候判断是否图片为空,因为有可能图片因为内存空间满了而被剔除
    if (cacheBitmap != null){
        //TODO
    }

我们在使用LruCache算法的时候,首先会对其分配一下内存空间。一般情况下我们都是使用运行内存的八分之一作为内存空间来存储数据。之后实例化LruCache,覆写里面的一个方法sizeOf,里面返回的为你存储进入缓存的每一个数据的大小。注意,此处的单位必须和你一开始分配的内存空间大小的单位保持一致。

这里需要说一个题外话,我们如何获取一个bitmap的内存大小?获取bitmap内存大小的方式有两种,一种为bitmap的高(bitmap.getHeight(),或者可以说为行数) * bitmap所占的字节数(bitmap.getRowByte())来获取,另外一种为直接调用bitmap.getByteCount()来获取。那么这两个有什么区别呐?其实区别就是bitmap.getRowByte()所支持的版本更低,而bitmap.getByteCount()内存的实现其实就是bitmap.getRowByte() * bitmap.getHeight();
public final int getByteCount() {
// int result permits bitmaps up to 46,340 x 46,340
return getRowBytes() * getHeight();
}

初始化完成LruCache对象之后,我们通过put方法将需要进行缓存的数据通过key和value的形式放入分配的内存中,之后需要使用的时候再通过get方法用key找到我们缓存的对象。不过这里需要注意一点,我们在获取到我们的缓存对象之后,不要急着去使用他,而是去先进行一次非空判断,因为有可能图片因为内存空间不足而被剔除掉。

LruCache算法源码解析

分析完了上面的内部原理和使用注意事项之后,我们再来通过源码来加深一下对LruCache算法的理解。

我们先看下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);
}

从构造方法的源码我们可以看到,在这端代码中我们主要做了两件事。第一是判断下传递来的最大分配内存大小是否小于零,如果小于零则抛出异常,因为我们如果传入一个小于零的内存大小就没有意义了。之后在构造方法内存就new了一个LinkHashMap集合,从而得知LruCache内部实现原理果然是基于LinkHashMap来实现的。

之后我们再来看下存储缓存的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++;
        size += safeSizeOf(key, value);
        previous = map.put(key, value);
        if (previous != null) {
            size -= safeSizeOf(key, previous);
        }
    }

    if (previous != null) {
        entryRemoved(false, key, previous, value);
    }

    trimToSize(maxSize);
    return previous;
}

从代码中我们可以看到,这个put方法内部其实没有做什么很特别的操作,就是对数据进行了一次插入操作。但是我们注意到最后的倒数第三行有一个trimToSize()方法,那么这个方法是做什么用的呐?我们点进去看下。
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);
    }
}

我们可以看到,这个方法原来就是对内存做了一次判断,如果发现内存已经满了,那么就调用map.eldest()方法获取到最后的数据,之后调用map.remove(key)方法,将这个最近最少使用的数据给剔除掉,从而达到我们内存不炸掉的目的。

我们再来看看get()方法。
public final V get(K key) {
//key为空抛出异常
if (key == null) {
throw new NullPointerException("key == null");
}

    V mapValue;
    synchronized (this) {
        //获取对应的缓存对象
        //get()方法会实现将访问的元素更新到队列头部的功能
        mapValue = map.get(key);
        if (mapValue != null) {
            hitCount++;
            return mapValue;
        }
        missCount++;
    }

get方法看起来就是很常规的操作了,就是通过key来查找value的操作,我们再来看看LinkHashMap的中get方法。
public V get(Object key) {
LinkedHashMapEntry<K,V> e = (LinkedHashMapEntry<K,V>)getEntry(key);
if (e == null)
return null;
//实现排序的关键方法
e.recordAccess(this);
return e.value;
}

调用recordAccess()方法如下:
void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
if (lm.accessOrder) {
lm.modCount++;
remove();
addBefore(lm.header);
}
}

由此可见LruCache中维护了一个集合LinkedHashMap,该LinkedHashMap是以访问顺序排序的。当调用put()方法时,就会在结合中添加元素,并调用trimToSize()判断缓存是否已满,如果满了就用LinkedHashMap的迭代器删除队尾元素,即最近最少访问的元素。当调用get()方法访问缓存对象时,就会调用LinkedHashMap的get()方法获得对应集合元素,同时会更新该元素到队头。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容