关闭

LruCache缓存图片研究小结

标签: androidlrucache图片缓存
501人阅读 评论(0) 收藏 举报
分类:

上一篇研究了LinkedHashMap实现LRU策略,虽然通过上述方式来实现图片缓存可以优化内存的使用效率,但是这种方式也存在一些问题,例如,LinkedHashMap不是线程安全的,所以在操作时需要考虑线程安全问题。另外在缓存时,只能指定缓存数据条目的数量,不能指定缓存区的大小,如果需要缓存的图片都比较大,可能就会出现问题。。。。

其实在Android SDK 中已经为我们提供了一个实现LRU策略的cache类,LruCache类,这个类封装了LinkedHashMap并且解决了LinkedHashMap中存在的问题,今天我们以LruCache缓存图片为例研究下这个类。

有一点需要提前说明下,今天我们研究的是android.support.v4.util包中的LruCache类而不是android.util包中也有一个LruCache类,这点需要提前说明下,不然很容易蒙圈,因为这两个类中有些方法实现是不同的。

首先我们看一下LruCache的成员变量,代码如下

    private final LinkedHashMap<K, V> map;

    private int size;//当前容量
    private int maxSize;//最大容量

    private int putCount;//put的次数 
    private int createCount;//create的次数 
    private int evictionCount;//回收次数
    private int hitCount;//命中次数
    private int missCount;//丢失的次数

再看下构造方法。

   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);
    }

在构造方法中初始化了maxSize和LinkedHashMap,并且LinkedHashMap为按照访问顺序排序(此部分请参照上一篇文章)。maxSize即为最大容量。

前面提到LruCache可以指定缓存区大小,这怎么实现呢?。。其实实现起来很简单只要在初始化的时候重写LruCache提供的sizeOf方法即可,代码如下。

        int LRU_CACHE_SIZE = 4 * 1024 * 1024; //4MB
        LruCache<String,Bitmap> lruCache=new LruCache<String,Bitmap>(LRU_CACHE_SIZE) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                if (value != null)
                    return value.getByteCount();
                else
                    return 0;
            }
        };

首先我们定义了LruCache的最大容量为4 * 1024 * 1024,即4M的空间,然后我们重写sizeOf方法,让这个方法返回Bitmap的字节数。为什么这样写就可以指定缓存区大大小呢?接下来我们看下比较重要的put和get方法,你就会明白了~

首先看下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;
    }

在方法体中,我们发现当key或者value为空时会抛出异常,这部分和LinkedHashMap是不同的,说明LruCache类是不支持key或者value为空,并且在对map进行put操作时加了synchronized ,保证了线程安全。另外我们发现有如下操作

size += safeSizeOf(key, value);

这部分看上去像对size(当前容量)的累加,我们继续进入到safeSizeOf方法中

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

在这个方法中看到了之前重写sizeOf方法,看到这里我想大家就明白了。我们之前重写sizeOf方法让它返回bitmap的字节数,而在put方法中会对每次put进来的bimap的字节数利用size进行累加,这样我们利用size和maxSize进行比较,就可以得知当前容量是否大于最大容量,当大于时就可以进行删除最近最少少用的资源的操作了。另外sizeOf默认情况下是返回1的,所以如果不重写sizeOf方法,LruCache也是进行“计数”的。

接着看put方法的代码,我们会发现如下操作。

       previous = map.put(key, value);
       if (previous != null) {
                size -= safeSizeOf(key, previous);
       }

我们都知道,在对一个hashmap进行put操作的时候 ,如果put的key-value键值对中的key已经存在与map中,新put的value会覆盖旧value,并且会返回旧value。如果key不存在于map中,则会返回null。这里利用previous获取返回值,如果previous不为空,则当前容量会减去previous的大小,这部分比较好理解,其实就是防止同一个key对应新旧value的大小的重复叠加。

接着往下看,在previous不为空的时候,会调用 entryRemoved(false, key, previous, value) 方法,并且把previous传进去了,查看此方法,我们发现此方法并没有函数体,看来需要我们重写此方法。

protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}

根据前面所讲,我们可以在初始化的时候重写这个方法,来释放掉一个key对应的旧value所占用的资源,代码如下。

 LruCache<String,Bitmap> lruCache=new LruCache<String,Bitmap>(LRU_CACHE_SIZE) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                if (value != null)
                    return value.getByteCount();
                else
                    return 0;
            }

            @Override
            protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
                if(evicted==false&&oldValue!=null){
                    oldValue.recycle();//释放bitmap资源
                }
            }
        };

看到这里,可能会有疑问,在什么方法里会进行删除最近最少的操作呢?接着看put方法我们会发现trimToSize方法,答案其实就在这个方法里面。代码如下。

public void trimToSize(int maxSize) {
        while (true) {//不断循环删除linkedHashMap首元素,也就是最近最少访问的条目,直到size小于最大容量或者map中已经没有数据
            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();//指向首元素
                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key);//删除最近最少的entry
                size -= safeSizeOf(key, value);
                evictionCount++;
            }

            entryRemoved(true, key, value, null);
        }
    }

观察方法体我们发现,当当前容量大于最大容量时,会不断删除首元素即最近最少访问的元素,然后重新计算当前容量大小。只有在当前容量小于最大容量或者map中没有数据的时候才会break出去。在代码的最后也会调用一次entryRemoved(true, key, value, null)方法,并且把因为空间不足而删除的元素的key和value传递进去,不过第一参数为true有别与先前调用时传递的false,所以根据第一参数区别我们就可以进行一些不一样的操作。

protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
                if(evicted==false&&oldValue!=null){
                    oldValue.recycle();
                }if(evicted==true&&oldValue!=null){
                    //TODO 根据key value 进行二级缓存
                }
            }

好了,put方法说明的比较详细,相信大家已经有了比较深的了解,接下来get方法我们加快点节奏。get方法源码如下。

public final V get(K key) {
        if (key == null) {//key 不允许为空
            throw new NullPointerException("key == null");
        }

        V mapValue;
        synchronized (this) {
            mapValue = map.get(key);
            if (mapValue != null) {
                hitCount++;
                return mapValue;
            }
            missCount++;
        }

        V createdValue = create(key);//根据key进行新建
        if (createdValue == null) {
            return null;
        }

        synchronized (this) {
            createCount++;
            mapValue = map.put(key, createdValue);

            if (mapValue != null) {//有返回值,说明key对应value已存在,需要进行重现赋值,即取消上一步操作
                map.put(key, mapValue);
            } else {
                size += safeSizeOf(key, createdValue);
            }
        }

        if (mapValue != null) {
            entryRemoved(false, key, createdValue, mapValue);//可以释放刚创建的createdValue
            return mapValue;
        } else {
            trimToSize(maxSize);
            return createdValue;
        }
    }

观察代码,我们发现其实lrucache方法中主要是对linkedhashmap进行get操作,这部分不清除的同学可以看下上一篇关于linkedhashmap的研究总结。

我们重点研究下通过key获取到的value为空时的case。观察代码,如果如果出现上述情况,会新建一个createdValue,并且把createdValue put到map中,利用mapValue 获取put返回值,继续判断mapValue 是否为空,如果mapValue 不为空说明此时此key在map中存在对应的value,所以接下来需要进行重新赋值,让key对应的value为mapValue 而不是createdValue。如果mapValue为空,说明key对应的value确实为空,需要进行就是重新计算当前size的大小。在代码的最后会判断mapValue是否为空,如果mapValue不为空的时候,会调用entryRemoved方法,并把createdValue放在oldvalue的位置,因为此时createdValue所占的资源是无用的,所以我们可以在entryRemoved中释放createdValue。如果mapValue不为空又会进入到trimToSize方法中进行容量计算和删除“最近最少”。好了,get也方法讲完了,最后看下remove方法。

   public final V remove(K key) {
        if (key == null) {
            throw new NullPointerException("key == null");
        }

        V previous;
        synchronized (this) {
            previous = map.remove(key);
            if (previous != null) {
                size -= safeSizeOf(key, previous);
            }
        }

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

        return previous;
    }

观察代码,基本上和get和put有异曲同工之妙,在这里就不重复阐述了,大家自己阅读代码吧!~~~

比较重要的方法都讲完了,到此大家对lrucache应该有了一个全面的了解了吧~~接下来我们稍微总结下lrucache吧。

1.LruCache封装了LinkedHashMap,提供了LRU缓存的功能,并且在关键操作加了synchronized ,实现了线程安全。
2.LruCache提供了trimToSize方法,当容量不足时会自动删除最近最少访问的键值对。
3.LruCache提供了entryRemoved(boolean evicted, K key, V oldValue, V newValue)方法,通过重写这个方,结合put,get,remove方法我们可以做更多的事情。
3.LruCache不允许空键值;
5.LruCache提供了sizeof方法,重写这个方法可以实现指定缓存区大小,而不是像LinkedHashMap一样只能指定缓存条目数。

ok!就写到这了,欢迎大家一起研究交流!~~~

0
0
查看评论

Android 图片缓存之内存缓存技术LruCache,软引用

转自:http://blog.chinaunix.net/uid-26930580-id-4138306.html
  • gf771115
  • gf771115
  • 2014-06-12 17:32
  • 23284

使用LruCache缓存,轻松解决图片过多造成的OOM

Android中一般情况下采取的缓存策略是使用二级缓存,即内存缓存+硬盘缓存—>LruCache+DiskLruCache,二级缓存可以满足大部分的需求了,另外还有个三级缓存(内存缓存+硬盘缓存+网络缓存),其中DiskLruCache就是硬盘缓存
  • Mr_wzc
  • Mr_wzc
  • 2016-05-25 12:18
  • 1801

安卓缓存之LruCache及设计(异步+缓存)图片加载器LruCacheImageLoader

一、LruCache LRU:Least Recently Used(近期最少使用)。LruCache基于LRU算法的缓存策略。LruCache是一个泛型类,其以强引用的方式存储外界的缓存对象。当内存缓存达到设定的最大值时,则将内存缓存中近期最少使用的对象移除,有效的避免了OOM的出现。Java...
  • cxmscb
  • cxmscb
  • 2016-08-25 15:09
  • 1470

【LruCache和DiskLruCache结合】图片缓存机制

本文是对网络上几篇文章的综合,
  • boyupeng
  • boyupeng
  • 2015-07-29 21:33
  • 8441

Android 图片缓存之内存缓存技术LruCache

转自:http://blog.chinaunix.net/uid-26930580-id-4138306.html 在Android中,有一个叫做LruCache类专门用来做图片缓存处理的。 它有一个特点,当缓存的图片达到了预先设定的值的时候,那么近期使用次数最少的图片就会被回收掉。 步骤:(...
  • qq_26222859
  • qq_26222859
  • 2016-02-27 19:14
  • 443

Android 关于使用LruCache缓存你想缓存的数据

又是好久没写博客。。 今天我们来一起学习一下缓存技术,相信大家做开发的时候都知道请求网络数据的重要,但是有一些只用请求一次就过时性的消息比如某些新闻信息,如果我们每次进入新闻界面就从新从网络上获取势必会给用户带来不好的体验,所以我们需要缓存技术来帮我们解决这一问题。 1,LruCache介绍 ...
  • u014163726
  • u014163726
  • 2015-02-06 10:22
  • 6919

Android缓存LruCache详解

转自:http://www.cnblogs.com/tianzhijiexian/p/4248677.html LruCache是android提供的一个缓存工具类,其算法是最近最少使用算法。它把最近使用的对象用“强引用”存储在LinkedHashMap中,并且把最近最少使用的对象在缓存值达到...
  • zhaicaixiansheng
  • zhaicaixiansheng
  • 2016-10-12 14:23
  • 1173

LruCache的实现原理(图片三级缓存)

官方建议使用lrucache进行内存缓存。Lrucache底层实际是维护的一个linkedHashMap集合(他是hashmap的一个子类,可以保证存入和取出顺序的集合,与hashmap不同的是他是一个双向链表从Android2.3以后,系统GC操作更加频繁,所以软引用和弱引用的资源很容易被回收。A...
  • IT_51888_liang
  • IT_51888_liang
  • 2016-02-28 00:41
  • 3438

Android 缓存浅谈(一) LruCache

Android应用开发好多场景都是手机和web服务器之间进行通信,从服务端需要获取数据,但是当访问的数据比较大,比较多,并且是重复数据时,会极大影响性能,甚至应用崩溃,手机卡死,这时候就要考虑缓存机制了!Android中可通过缓存来减少频繁的网络操作,减少流量、提升性能。    ...
  • zxw136511485
  • zxw136511485
  • 2016-08-11 10:37
  • 8506

内存缓存LruCache详解

前言最近有用到LruCache,但是对其原理不太了解,所以看了源码,知道了一个大概,想总结一下!介绍现在大部分的缓存框架,比如图片加载框架,网络请求框架等都使用三级缓存来提高效率,即内存-文件(SD卡或手机)-网络。对于图片加载来说,就是加载图片的时候首先从内存缓存中取,如果没有再从文件缓存中取,如...
  • aiynmimi
  • aiynmimi
  • 2016-11-02 15:17
  • 1562
    个人资料
    • 访问:14586次
    • 积分:338
    • 等级:
    • 排名:千里之外
    • 原创:15篇
    • 转载:0篇
    • 译文:0篇
    • 评论:5条
    文章分类