12.图片三级缓存和LruCache源码

简介: 大多的开源图片框架针对图片加载都采用了三级缓存的方式,大概流程通常是这样的,加载图片时,首先检查内存中是否仍然保有这个图片对象,如果有则直接显示到控件上,加载过程到此结束;如果内存中没有,则可能是第一次加载,还没有缓存或者内存中的缓存被销毁,这时候去本地缓存中读取,通常是写入到了文件中,如果文件中读取到了缓存,则设置给控件显示,加载结束,如果没有缓存,则再请求服务器返回,这时候会将获取到的图片写入到本地硬盘(文件)中,(或者同时在内存中也写入一份),同时设置给图片显示。

大多的开源图片框架针对图片加载都采用了三级缓存的方式,大概流程通常是这样的,加载图片时,首先检查内存中是否仍然保有这个图片对象,如果有则直接显示到控件上,加载过程到此结束;如果内存中没有,则可能是第一次加载,还没有缓存或者内存中的缓存被销毁,这时候去本地缓存中读取,通常是写入到了文件中,如果文件中读取到了缓存,则设置给控件显示,加载结束,如果没有缓存,则再请求服务器返回,这时候会将获取到的图片写入到本地硬盘(文件)中,(或者同时在内存中也写入一份),同时设置给图片显示。这时第一次加载结束,下次再次加载时,重复上一个过程,只要存在缓存就不再请求网络,降低不必要的网络请求。如下图

img_d13fe2ead51ec851f7ec43727af4b5e9.png
图片三级缓存.png

简易版的三级缓存框架: https://github.com/renzhenming/BitmapUtils

内存缓存是如何实现的,我们当然可以用一个HashMap来存储获取到的bitmap,以url的md5值为key来保存,但是有一个问题需要注意,安卓系统为每一个应用分配的内存都是有限的,使用HashMap固然可以实现功能,但是当图片足够多的时候,HashMap无法为你清理内存,极有可能发生内存溢出。

为了防止这种问题,可以在把bitmap加如到集合中时,使用软引用,弱引用,虚引用等包裹bitmap,这样可以防止内存溢出,及时的清理bitmap,但有一个问题,这样内存缓存的作用就不存在了,我们的目的是做缓存,但从 Android 2.3 (API Level 9)开始,垃圾回收器会更倾向于回收持有软引用或弱引用的对象,软引用和弱引用已经不再可靠了,试想如果你刚刚加入缓存就被系统清理了,达不到我们想要的效果,所以Android系统提供了一个可靠的缓存集合LruCache,LruCache内部封装了一个LinkedHashMap集合,所以也可以把它当作集合来看待

        long maxMemory = Runtime.getRuntime().maxMemory();//获取Dalvik 虚拟机最大的内存大小:16

        LruCache<String, Bitmap> lruCache = new LruCache<String,Bitmap>((int) (maxMemory/8)){//指定内存缓存集合的大小
            //获取图片的大小
            @Override
            protected int sizeOf(String key, Bitmap value) {
                return value.getRowBytes()*value.getHeight();
            }
        };

LruCache创建的时候通过泛型指定map集合的key和value类型,并且通过构造方法传入设定的内存缓存集合的最大值,我们来看看它的构造方法,可以看到,这里保存了内存的最大占用空间,并且创建了一个LinkedHashMap,相比于HashMap,保证有序性

    /**
     * @param maxSize for caches that do not override {@link #sizeOf}, this is
     *     the maximum number of entries in the cache. For all other caches,
     *     this is the maximum sum of the sizes of the entries in this cache.
     */
    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);
    }

当网LruCache中添加内容的时候,进入put方法

/**
     * Caches {@code value} for {@code key}. The value is moved to the head of
     * the queue.
     *
     * @return the previous value mapped by {@code key}.
     */
    public final V put(K key, V value) {
        if (key == null || value == null) {
            throw new NullPointerException("key == null || value == null");
        }

        V previous;
        synchronized (this) {
            //每加如一个缓存对象,缓存计数器增加1
            putCount++;
            //计算出当前缓存的大小值,safeSizeOf就是上边重写的sizeOf
            //方法的封装,得到的是当前加如的缓存对象的大小,然后累加得到总大小
            size += safeSizeOf(key, value);
            //如果缓存中存在相同的对象,总的缓存大小减去当前这个存入的大小
            //也就是重复的缓存对象不计入缓存,map集合在put的时候,如果集合中存在的
            //话会用新的value值替换旧的value,不存在重复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;
    }

trimToSize
每次向LruCache中添加新的对象缓存时,都会检查一次当前缓存大小是否超过了设置的最大值,这是一个死循环,只要占用的空间大小极值,就会一直根据lru算法来得到最符合移除条件的一个对象然后移除它,直到内存大小在合理范围内

/**
     * Remove the eldest entries until the total of remaining entries is at or
     * below the requested size.
     *
     * @param maxSize the maximum size of the cache before returning. May be -1
     *            to evict even 0-sized elements.
     */
    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;
                }
                //根据lru算法(Least recently used,最近最少使用)得到一个entry
                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);
        }
    }

看到这里可以知道,其实LruCache的关键在于LinkedHashMap内部是如何运转的,它会根据lru算法,获取到最符合移除条件的一个对象,eldest()方法返回的就是我们需要的值,那么究竟是如何判断的呢,只有进入源码去看一下了

在LruCache实例化的时候,我们看到LinkedHashMap是这样构建的,关键在于最后一个true的标记,这个值代表什么?

 this.map = new LinkedHashMap<K, V>(0, 0.75f, true);

LinkedHashMap有五种构造方法,前四个构造方法都将accessOrder设为false,默认是按照插入顺序排序的;而第五个构造方法可以自定义传入的accessOrder的值,因此可以指定双向循环链表中元素的排序规则。特别地,当我们要用LinkedHashMap实现LRU算法时,就需要调用该构造方法并将accessOrder置为true。本质上,LinkedHashMap = HashMap + 双向链表,可以这样理解,LinkedHashMap 在不对HashMap做任何改变的基础上,给HashMap的任意两个节点间加了两条连线(before指针和after指针),使这些节点形成一个双向链表。在LinkedHashMapMap中,所有put进来的Entry都保存在HashMap中,并且对于每次put进来Entry还会将其插入到双向链表的尾部。

我们看看设置为true或者false,链表的存取有什么区别,找到put方法,LinkedHashMap的put方法使用的是父类HashMap的put

public V put(K key, V value) {
       return putVal(hash(key), key, value, false, true)
  }

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //这里可以看到,当存在相同的key时,会将新的value替换旧的value,并将旧的返回
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                //走到了这个方法,但其实这个方法在HashMap中是空的,LinkedHashMap重写了这个
                //方法,我们看看它是如何实现
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

可以看到,LinkedHashMap调用put方法添加数据的时候,除了继承了HashMap把数据添加到hash表中,还做了另一步操作,就是同时加入了它内部的一个双向链表中,并且是加入了尾部,如果是第一个加如得数据自然也就是头部了,那么这时差不多明白了,LinkedHashMap方法得eldest方法返回得值怎么就是最近最少使用得呢

void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMapEntry<K,V> last;
        //这里是构造LinkedHashMap时传入的标记位,只有为true的情况才会加如链表,
        //也就是说只有第五种构造方法被调用并且accessOrder 赋值为true,链表才会起作用
        //同时判断一个条件,当前要加如链表的value和当前链表中最后一个value是否相同
        //只有不同的情况才会将其加入
        if (accessOrder && (last = tail) != e) {
            //注意这种结构,p b a都是LinkedHashMapEntry类型,这一行代码写的很简洁
            //第一,将e强转为LinkedHashMapEntry p,第二将p在链表中的位置放在b的后边
            //(b = p.before),将p的后边指定为a,这里可以猜想b就是上一次放入链表中的
            //LinkedHashMapEntry
            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;
        }
    }

上边是存入得时候,那么取出得时候呢

/**
     * Returns the value to which the specified key is mapped,
     * or {@code null} if this map contains no mapping for the key.
     *
     * <p>More formally, if this map contains a mapping from a key
     * {@code k} to a value {@code v} such that {@code (key==null ? k==null :
     * key.equals(k))}, then this method returns {@code v}; otherwise
     * it returns {@code null}.  (There can be at most one such mapping.)
     *
     * <p>A return value of {@code null} does not <i>necessarily</i>
     * indicate that the map contains no mapping for the key; it's also
     * possible that the map explicitly maps the key to {@code null}.
     * The {@link #containsKey containsKey} operation may be used to
     * distinguish these two cases.
     */
    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存取数据都会调用afterNodeAccess方法,将最近使用得value放在链表得末尾,那么这样一来就很清楚了,使用最多得一直放在链表得末尾,使用最少得自然就放在头部了,那么eldest方法返回得head自然也就是最满足移除条件得最少使用得value了。

LinkedHashMap重写了HashMap中的afterNodeAccess方法(HashMap中该方法为空),当调用父类的put方法时,在发现key已经存在时,会调用该方法;当调用自己的get方法时,也会调用到该方法。该方法提供了LRU算法的实现,它将最近使用的Entry放到双向循环链表的尾部。也就是说,当accessOrder为true时,get方法和put方法都会调用recordAccess方法使得最近使用的Entry移到双向链表的末尾。

此时可以回到最初得位置了,LruCache中通过这一行代码取得要移除得对象,从而保证将内存控制在合理范围内。最根本的实现在于LinkedHashMap。

 Map.Entry<K, V> toEvict = map.eldest();
相关文章
|
2月前
|
存储 缓存 算法
缓存优化利器:5分钟实现 LRU Cache,从原理到代码!
嗨,大家好!我是你们的技术小伙伴——小米。今天带大家深入了解并手写一个实用的LRU Cache(最近最少使用缓存)。LRU Cache是一种高效的数据淘汰策略,在内存有限的情况下特别有用。本文将从原理讲起,带你一步步用Java实现一个简单的LRU Cache,并探讨其在真实场景中的应用与优化方案,如线程安全、缓存持久化等。无论你是初学者还是有一定经验的开发者,都能从中受益。让我们一起动手,探索LRU Cache的魅力吧!别忘了点赞、转发和收藏哦~
43 2
|
5月前
|
缓存 NoSQL Java
springboot集成图片验证+redis缓存一步到位2
springboot集成图片验证+redis缓存一步到位2
|
5月前
|
缓存 NoSQL Java
springboot集成图片验证+redis缓存一步到位
springboot集成图片验证+redis缓存一步到位
|
5月前
|
XML 缓存 Java
Android App开发之利用Glide实现图片的三级缓存Cache讲解及实战(附源码 超详细必看 简单易懂)
Android App开发之利用Glide实现图片的三级缓存Cache讲解及实战(附源码 超详细必看 简单易懂)
284 0
|
缓存 Java Android开发
Android使用LruCache、DiskLruCache实现图片缓存+图片瀑布流
**本文仅用于学习利用LruCache、DiskLruCache图片缓存策略、实现瀑布流和Matix查看大图缩放移动等功能,如果想用到项目中,建议用更成熟的框架,如[glide]
158 0
|
存储 缓存 算法
Android内存缓存LruCache源码解析
内存缓存,使用强引用方式缓存有限个数据,当缓存的某个数据被访问时,它就会被移动到队列的头部,当一个新数据要添加到LruCache而此时缓存大小要满时,队尾的数据就有可能会被垃圾回收器(GC)回收掉,LruCache使用的LRU(Least Recently Used)算法,即:<strong>把最近最少使用的数据从队列中移除,把内存分配给最新进入的数据。</strong>
|
缓存 Java
Android--SoftReference缓存图片
Android--SoftReference缓存图片
314 2
|
缓存 算法 Java
Android 内存缓存框架 LruCache 的实现原理,手写试试?
在之前的文章里,我们聊到了 LRU 缓存淘汰算法,并且分析 Java 标准库中支持 LUR 算法的数据结构 LinkedHashMap。当时,我们使用 LinkedHashMap 实现了简单的 LRU Demo。今天,我们来分析一个 LRU 的应用案例 —— Android 标准库的 LruCache 内存缓存。
178 0
|
存储 缓存 NoSQL
漫谈 LevelDB 数据结构(三):LRU 缓存( LRUCache)
漫谈 LevelDB 数据结构(三):LRU 缓存( LRUCache)
402 0
漫谈 LevelDB 数据结构(三):LRU 缓存( LRUCache)
|
缓存 应用服务中间件 nginx
Nginx代理阿里云OSS图片 并设置缓存
Nginx代理阿里云OSS图片 并设置缓存
1724 0