一文带你分析LruCache源码


LruCache,首先从名字就可以看出它的功能。作为较为常用的缓存策略,它在日常开发中起到了重要的作用。例如Glide中,它与SoftReference 在Engine类中缓存图片,可以减少流量开销,提升加载图片的效率。在API12时引入android.util.LruCache,然而在API22时对它进行了修改,引入了android.support.v4.util.LruCache。我们在这里分析的是support包里的LruCache


什么是LruCache算法?

Lru(Least Recently Used),也就是最近最少使用算法。它在内部维护了一个LinkedHashMap,在put数据的时候会判断指定的内存大小是否已满。若已满,则会使用最近最少使用算法进行清理。至于为什么要使用LinkedHashMap存储,因为LinkedHashMap内部是一个数组加双向链表的形式来存储数据,也就是说当我们通过get方法获取数据的时候,数据会从队列跑到队头来。反反复复,队尾的数据自然是最少使用到的数据。



image


LruCache如何使用?


初始化

一般来说,我们都是取运行时最大内存的八分之一来作为内存空间,同时还要覆写一个sizeOf的方法。特别需要强调的是,sizeOf的单位必须和内存空间的单位一致。

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


API

公共方法
final int createCount()返回返回值的次数create(Object)
final void evictAll()清除缓存,调用entryRemoved(boolean, K, V, V)每个删除的条目。
final int evictionCount()返回已被驱逐的值的数量。
final V get(K key)返回key缓存中是否存在的值,还是可以创建的值#create
final int hitCount()返回返回get(K)已存在于缓存中的值的次数。
final int maxSize()对于不覆盖的高速缓存sizeOf(K, V),这将返回高速缓存中的最大条目数。
final int missCount()返回get(K)返回null或需要创建新值的次数。
final V put(K key, V value)缓存valuekey
final int putCount()返回put(K, V)调用的次数。
final V remove(K key)删除条目(key如果存在)。
void resize(int maxSize)设置缓存的大小。
final int size()对于不覆盖的高速缓存sizeOf(K, V),这将返回高速缓存中的条目数。
final Map<K, V> snapshot()返回缓存的当前内容的副本,从最近最少访问到最近访问的顺序排序。
final String toString()
void trimToSize(int maxSize)删除最旧的条目,直到剩余条目的总数等于或低于请求的大小。


LruCache源码分析

我们接下里从构造方法开始为大家进行讲解:

构造函数

    public LruCache(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        } else {
            this.maxSize = maxSize;
            this.map = new LinkedHashMap(0, 0.75F, true);
        }
    }

构造函数一共做了两件事。第一节:判断maxSize是否小于等于0。第二件,初始化maxSize和LinkedHashMap。没什么可说的,我们接着往下走。

safeSizeOf(测量元素大小)

    private int safeSizeOf(K key, V value) {
        int result = this.sizeOf(key, value);
        if (result < 0) {//判空
            throw new IllegalStateException("Negative size: " + key + "=" + value);
        } else {
            return result;
        }
    }

sizeOf (测量元素大小)

这个方法一定要覆写,否则存不进数据。

    protected int sizeOf(@NonNull K key, @NonNull V value) {
        return 1;
    }

put方法 (增加元素)

@Nullable
    public final V put(@NonNull K key, @NonNull V value) {
        if (key != null && value != null) {
            Object previous;
            synchronized(this) {
                ++this.putCount;//count为LruCahe的缓存个数,这里加一
                this.size += this.safeSizeOf(key, value);//加上这个value的大小
                previous = this.map.put(key, value);//存进LinkedHashMap中
                if (previous != null) {//如果之前存过这个key,则减掉之前value的大小
                    this.size -= this.safeSizeOf(key, previous);
                }
            }

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

            this.trimToSize(this.maxSize);//进行内存判断
            return previous;
        } else {
            throw new NullPointerException("key == null || value == null");
        }
    }

在synchronized代码块里,进入的就是一次插入操作。我们往下,俺老孙定眼一看,似乎trimToSize这个方法有什么不寻常的地方?

trimToSize (判断是否内存溢出)

public void trimToSize(int maxSize) {
        while(true) {//这是一个无限循环,目的是为了移除value直到内存空间不溢出
            Object key;
            Object value;
            synchronized(this) {
                if (this.size < 0 || this.map.isEmpty() && this.size != 0) {//如果没有分配内存空间,抛出异常
                    throw new IllegalStateException(this.getClass().getName() + ".sizeOf() is reporting inconsistent results!");
                }

                if (this.size <= maxSize || this.map.isEmpty()) {//如果小于内存空间,just so so~
                    return;
                }
                //否则将使用Lru算法进行移除
                Entry<K, V> toEvict = (Entry)this.map.entrySet().iterator().next();
                key = toEvict.getKey();
                value = toEvict.getValue();
                this.map.remove(key);
                this.size -= this.safeSizeOf(key, value);
                ++this.evictionCount;//回收次数+1
            }

            this.entryRemoved(true, key, value, (Object)null);
        }
    }

这个TrimToSize方法的作用在于判断内存空间是否溢出。利用无限循环,将一个一个的最少使用的数据给剔除掉。

get方法 (获取元素)

@Nullable
    public final V get(@NonNull K key) {
        if (key == null) {
            throw new NullPointerException("key == null");
        } else {
            Object mapValue;
            synchronized(this) {
                mapValue = this.map.get(key);
                if (mapValue != null) {
                    ++this.hitCount;//命中次数+1,并且返回mapValue
                    return mapValue;
                }

                ++this.missCount;//未命中次数+1
            }
            /*
            如果未命中,会尝试利用create方法创建对象
            create需要自己实现,若未实现则返回null
            */
            V createdValue = this.create(key);
            if (createdValue == null) {
                return null;
            } else {
                synchronized(this) {
                    //创建了新对象之后,再将其添加进map中,与之前put方法逻辑基本相同
                    ++this.createCount;
                    mapValue = this.map.put(key, createdValue);
                    if (mapValue != null) {
                        this.map.put(key, mapValue);
                    } else {
                        this.size += this.safeSizeOf(key, createdValue);
                    }
                }

                if (mapValue != null) {
                    this.entryRemoved(false, key, createdValue, mapValue);
                    return mapValue;
                } else {
                    this.trimToSize(this.maxSize);//每次加入数据时,都需要判断一下是否溢出
                    return createdValue;
                }
            }
        }
    }

create方法 (尝试创造对象)

    @Nullable
    protected V create(@NonNull K key) {
        return null;//这个方法需要自己实现
    }

get方法和create方法的注释已经写在了代码上,这里逻辑同样不是很复杂。但是我们需要注意的是map的get方法,既然LinkedHashMap能实现Lru算法,那么它的内部一定不简单!

LinkedHashMap的get方法

public V get(Object key) {
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) == null)
            return null;
        if (accessOrder)
            afterNodeAccess(e);
        return e.value;
    }

LinkedHashMap中,首先进行了判断,是否找到该元素,没找到则返回null。找到则调用afterNodeAccess方法。

LinkedHashMap的afterNodeAccess方法

void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMapEntry<K,V> last;
            //accessOrder为true 且当前节点不是尾节点 则按访问顺序排序
        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;
        }
    }

原来如此!LinkedHashMap在这个方法中实现了按访问顺序排序,这也就是为什么我们的LruCache底层是使用的LinkedHashMap作为数据结构。

主要方法已经讲完了 ,接下里我们就看看其他方法吧。

remove (移除元素)

    @Nullable
    public final V remove(@NonNull K key) {
        if (key == null) {//判空
            throw new NullPointerException("key == null");
        } else {
            Object previous;
            synchronized(this) {
                previous = this.map.remove(key);//根据key移除value
                if (previous != null) {
                    this.size -= this.safeSizeOf(key, previous);//减掉value的大小
                }
            }

            if (previous != null) {
                this.entryRemoved(false, key, previous, (Object)null);
            }

            return previous;
        }
    }

evictAll方法(移除所有元素)

    public final void evictAll() {
        this.trimToSize(-1);//移除掉所有的value
    }

其他方法

    public final synchronized int size() {
        return this.size;//当前内存空间的size
    }

    public final synchronized int maxSize() {
        return this.maxSize;//内存空间最大的size
    }

    public final synchronized int hitCount() {
        return this.hitCount;//命中个数
    }

    public final synchronized int missCount() {
        return this.missCount;//未命中个数
    }

    public final synchronized int createCount() {
        return this.createCount;//创建Value的个数
    }

    public final synchronized int putCount() {
        return this.putCount;//put进去的个数
    }

    public final synchronized int evictionCount() {
        return this.evictionCount;//移除个数
    }

    public final synchronized Map<K, V> snapshot() {
        return new LinkedHashMap(this.map);//创建LinkedHashMap
    }

    public final synchronized String toString() {//toString
        int accesses = this.hitCount + this.missCount;
        int hitPercent = accesses != 0 ? 100 * this.hitCount / accesses : 0;
        return String.format(Locale.US, "LruCache[maxSize=%d,hits=%d,misses=%d,hitRate=%d%%]", this.maxSize, this.hitCount, this.missCount, hitPercent);
    }

最后

有了这篇文章,相信大家对LruCache的工作原理已经很清楚了吧!有什么不对的地方希望大家能够指正。学无止境,大家一起加油吧。

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

推荐阅读更多精彩内容