Android内存缓存管理LruCache源码解析与示例

来源:互联网 发布:mac微信好用吗 编辑:程序博客网 时间:2024/06/06 20:05

在深圳出差,非常忙,抽空写文章,这些文章的质量很可能不高,但还是希望可以帮到你。

在加载图片的时候,我们要考虑到内存问题,(内存缓存作为最先被读取的数据,应该存储那些经常使用的数据对象,且内存容量有限,内存缓存的容量应该限定。)如果你加载是高清无码大图很可能会造成OOM,那我们需要一个东西来管理这个图片与其缓存。

今天我们来讲一下LruCache的原理及实现,这个谷歌推荐的内存缓存的方法。

那么Lru是什么?

LRU全称为Least Recently Used,即最近最少使用,是一种缓存置换算法。(多加一句LFU(least frequently used )算法,则淘汰的是最不经常使用的)。

问题:当有新的内容需要加入我们的缓存,但我们的缓存空闲的空间不足以放进新的内容时,如何舍弃原有的部分内容从而腾出空间用来放新的内容。

让我们进入一下LruCache的学习

首先让我们看一下使用方法中的初始化MemoryCache:

public class BitmapMemoryCache {    private static final String TAG = "BitmapMemoryCache";    private static BitmapMemoryCache sInstance = new BitmapMemoryCache();    private LruCache<String, Bitmap> mMemoryCache;    public Map<String, SoftReference<Bitmap>> mImageCacheMap = new HashMap<String, SoftReference<Bitmap>>();    /*单例模式*/    public  BitmapMemoryCache getInstance() {        return BitmapMemoryCache.sInstance;    }    private BitmapMemoryCache() {        int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);//获取系统分配给应用的总内存大小,不是获取系统全部的的        int cacheSize = maxMemory / 8;//设置图片内存缓存占用八分之一,要依据你申请下来的和你估算使用的大小来        Log.e(TAG, "" + cacheSize);        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {            @Override            protected int sizeOf(String key, Bitmap bitmap) {                // 重写此方法来衡量每张图片的大小,默认返回图片数量。                Log.w(TAG, "addBitmapTo " + (bitmap.getByteCount() / 1024));                return bitmap.getByteCount() / 1024;            }        };    }  }

接着让我们进入LruCache源码

明显的可以看出LruCache是利用了LinkedHashMap

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;//当前缓存内容的大小。    private int maxSize; // 最大可缓存的大小    private int putCount;// put方法被调用的次数    private int createCount;//create(Object) 被调用的次数    private int evictionCount;//被置换出来的元素的个数    private int hitCount; //get方法获取到缓存中的元素的次数    private int missCount;//get方法未获取到缓存中元素的次数    /**     * @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);    }    /**     * Sets the size of the cache.     *//设置缓存大小     * @param maxSize The new maximum size.     */    public void resize(int maxSize) {        if (maxSize <= 0) {            throw new IllegalArgumentException("maxSize <= 0");        }        synchronized (this) {            this.maxSize = maxSize;        }        trimToSize(maxSize);    }  }

走到这里我们有个疑问—LinkedHashMap是什么?它是怎么实现LRU这种缓存策略的?

看文中的一句代码:

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

让我们进入LinkedHashMap源码来看一下。

进入构造方法查看。

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

参数说明:

  • initialCapacity 初始容量大小,使用无参构造方法时,此值默认是4(安卓SdkVersion 24中默认4,这里是使用了父类HashMap的默认值)
  • loadFactor 加载因子,使用无参构造方法时,此值默认是 0.75f(安卓SdkVersion 24中默认0.75,这里是使用了父类HashMap的默认值)
  • accessOrder false: 基于插入顺序 true: 基于访问顺序

LinkedHashMap继承自HashMap,不同的是,它是一个双向循环链表,它的每一个数据结点都有两个指针,分别指向直接前驱和直接后继,这一个我们可以从它的内部类LinkedEntry中看出,其定义如下:

 /**     private static class LinkedHashMapEntry<K,V> extends HashMapEntry<K,V> {        // These fields comprise the doubly linked list used for iteration.        LinkedHashMapEntry<K,V> before, after;    //一个双向循环链表,它的每一个数据结点都有两个指针,    //分别指向直接前驱和直接后继        LinkedHashMapEntry(int hash, K key, V value, HashMapEntry<K,V> next) {            super(hash, key, value, next);        }        private void remove() {            before.after = after;            after.before = before;        }      // Inserts this entry before the specified existing entry in the list.        private void addBefore(LinkedHashMapEntry<K,V> existingEntry) {            after  = existingEntry;            before = existingEntry.before;            before.after = this;            after.before = this;        }        void recordAccess(HashMap<K,V> m) {            LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;            if (lm.accessOrder) {                lm.modCount++;                remove();                addBefore(lm.header);            }        }        void recordRemoval(HashMap<K,V> m) {            remove();        }    }

LinkedHashMap实现了双向循环链表的数据结构。

  • 1,当链表不为空时,header.after指向第一个结点,header.before指向最后一个结点;

  • 2,当链表为空时,header.after与header.before都指向它本身。

    @Override

    void init() {

    header = new LinkedHashMapEntry<>(-1, null, null,null);

    header.before = header.after = header;

    }

  • accessOrder是指定它的排序方式,当它为false时,只按插入的顺序排序,即新放入的顺序会在链表的尾部;而当它为true时,更新或访问某个节点的数据时,这个对应的结点也会被放到尾部。

  • 它通过构造方法public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)来赋值。

我们加入一个新结点来看一下方法执行过程(在LinkedHashMap中):

   void addEntry(int hash, K key, V value, int bucketIndex) {        // Previous Android releases called removeEldestEntry() before actually        // inserting a value but after increasing the size.        // The RI is documented to call it afterwards.        // **** THIS CHANGE WILL BE REVERTED IN A FUTURE ANDROID RELEASE ****        //这个地方我专门去看了24和25,他并没有改,不知什么原因,欺骗我的感情        // Remove eldest entry if instructed  如果得到通知,移除最旧的        LinkedHashMapEntry<K,V> eldest = header.after;        if (eldest != header) {            boolean removeEldest;            size++;            try {                removeEldest = removeEldestEntry(eldest);            } finally {                size--;            }            if (removeEldest) {                removeEntryForKey(eldest.key);            }        }        super.addEntry(hash, key, value, bucketIndex);//调用父类的添加方法    }

好,我们来看一下父类的添加方法:

想要理解HashMap可以看我的这篇HashMap源码解析

    //父类的这个方法也是将新添加的放入尾部,这里既是链表的尾部 void addEntry(int hash, K key, V value, int bucketIndex) {        if ((size >= threshold) && (null != table[bucketIndex])) {            resize(2 * table.length);            hash = (null != key) ? sun.misc.Hashing.singleWordWangJenkinsHash(key) : 0;            bucketIndex = indexFor(hash, table.length);        }        createEntry(hash, key, value, bucketIndex);    }    void createEntry(int hash, K key, V value, int bucketIndex) {        HashMapEntry<K,V> e = table[bucketIndex];        table[bucketIndex] = new HashMapEntry<>(hash, key, value, e);        size++;    }

当我们加入新的元素之后,链表的顺序如图:

那么当我们访问了或者是更新了某个元素(当accessOrder为true时),链表里的元素位置怎么变化呢?

让我们来看一下get(Object key)方法的流程:

 public V get(Object key) {        LinkedHashMapEntry<K,V> e = (LinkedHashMapEntry<K,V>)getEntry(key);//获取LinkedHashMapEntry        if (e == null)            return null;        e.recordAccess(this);//此方法记录下来,并重新排序        return e.value;    }

进入recordAccess()查看:

这个方法是在LinkedHashMapEntry内部:

 //从这段代码中我们看到,首先执行remove,在执行addBefore LinkedHashMapEntry<K,V> before, after;void recordAccess(HashMap<K,V> m) {            LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;            if (lm.accessOrder) {                lm.modCount++;                remove();                addBefore(lm.header);            }        } private void remove() {//这是将此节点取出,如图一            before.after = after;            after.before = before;        //这有点绕,按照我的图片来捋一遍        //  node1.after=node3;        //  node3.before=node1        }  private void addBefore(LinkedHashMapEntry<K,V> existingEntry) {//此处将header传入,将操作的节点放置链表末尾            after  = existingEntry;            before = existingEntry.before;            before.after = this;            after.before = this;        }

图一:

是不是理解了LinkedHashMap的排序原理了?

熟悉了LinkedHashMap,让我们来分析LruCache;

我们发现,通过它来实现Lru算法也就变得理所当然了。我们所需要做的,就只剩下定义缓存的最大大小,记录缓存当前大小,在放入新数据时检查是否超过最大大小。

所以LruCache定义了以下三个必需的成员变量:

   private final LinkedHashMap<K, V> map;    /** Size of this cache in units. Not necessarily the number of elements. */    private int size;//当前缓存内容的大小。    private int maxSize; // 最大可缓存的大小

让我们来解析一下它的get方法:

  public final V get(K key) {        if (key == null) {            throw new NullPointerException("key == null");        }        V mapValue;        synchronized (this) {            mapValue = map.get(key);            if (mapValue != null) {// 当能获取到对应的值时,返回该值                hitCount++;//获取到缓存中的元素的次数+1,在文章头部有这几个参数的介绍                return mapValue;            }            missCount++;//未获取到缓存中的元素的次数+1        }        V createdValue = create(key);        if (createdValue == null) {            return null;//如果没有为key创建新值成功,则直接返回null        }        synchronized (this) {            createCount++;            mapValue = map.put(key, createdValue); //create调用次数+1  将创建的值放入map中,如果map在前面的过程中正好放入了这对key-value,那么会返回放入的value            if (mapValue != null) {                // There was a conflict so undo that last put                map.put(key, mapValue);        /如果不为null,说明不需要我们所创建的值,所以把返回的值放进去            } else {                size += safeSizeOf(key, createdValue);        //为null,说明我们更新了这个key的值,需要重新计算大小            }        }       if (mapValue != null) {//上面放入的值有冲突            entryRemoved(false, key, createdValue, mapValue);// 移除之前创建的值,改为mapValue            return mapValue;        } else {//没有冲突时,因为放入了新创建的值,大小已经有变化,所以需要调整大小            trimToSize(maxSize);            return createdValue;        }    }

LruCache是可能被多个线程同时访问的,所以在读写map时进行加锁。

当获取不到对应的key的值时,它会调用其create(K key)方法,这个方法用于当缓存没有命名时计算一个key所对应的值,它的默认实现是直接返回null。

这个方法并没有加上同步锁,也就是在它进行创建时,map可能已经有了变化。

所以在get方法中,如果create(key)返回的V不为null,会再把它给放到map中,并检查是否在它创建的期间已经有其他对象也进行创建并放到map中了,

如果有,则会放弃这个创建的对象,而把之前的对象留下,否则因为我们放入了新创建的值,所以要计算现在的大小并进行trimToSize。

trimToSize方法是根据传进来的maxSize,如果当前大小超过了这个maxSize,则会移除最老的结点,直到不超过。

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 || map.isEmpty()) {                    break;                }                Map.Entry<K, V> toEvict = map.entrySet().iterator().next();                key = toEvict.getKey();                value = toEvict.getValue();                map.remove(key);                size -= safeSizeOf(key, value);                evictionCount++;            }            entryRemoved(true, key, value, null);        }    }

接下来,我们再来看LruCach的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;    }

主要逻辑是,计算新增加的大小,加入size,

然后把key-value放入map中,如果是更新旧的数据(map.put(key, value)会返回之前的value),则减去旧数据的大小,并调用entryRemoved(false, key, previous, value)方法通知旧数据被更新为新的值。

最后也是调用trimToSize(maxSize)修整缓存的大小。

文末附上我以前写的一个管理类

/** * Bitmap缓存,简单缓存. * Created by ChangMingShan on 2015/12/26. */public class BitmapMemoryCache {    private static final String TAG = "BitmapMemoryCache";    private static BitmapMemoryCache sInstance = new BitmapMemoryCache();    private LruCache<String, Bitmap> mMemoryCache;    /**     * 单例模式.     */    public static BitmapMemoryCache getInstance() {        return BitmapMemoryCache.sInstance;    }    private BitmapMemoryCache() {        int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);        int cacheSize = maxMemory / 8;        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {            @Override            protected int sizeOf(String key, Bitmap bitmap) {                // 重写此方法来衡量每张图片的大小,默认返回图片数量。                return bitmap.getByteCount() / 1024;            }        };    }    public synchronized void addBitmapToMemoryCache(String key, Bitmap bitmap) {        if (mMemoryCache.get(key) == null) {            if (key != null && bitmap != null)                mMemoryCache.put(key, bitmap);        } else            Log.w(TAG, "the res is aready exits");    }    public synchronized Bitmap getBitmapFromMemCache(String key) {        Bitmap bm = mMemoryCache.get(key);        if (key != null) {            return bm;        }        return null;    }    /**     * 移除缓存     *     * @param key     */    public synchronized void removeImageCache(String key) {        if (key != null) {            if (mMemoryCache != null) {                Bitmap bm = mMemoryCache.remove(key);                if (bm != null)                    bm.recycle();            }        }    }     /**     * 移除缓存     */    public synchronized void clearImageCache() {        if (mMemoryCache != null) {            if (mMemoryCache.size() > 0) {                Log.d("CacheUtils",                        "mMemoryCache.size() " + mMemoryCache.size());                mMemoryCache.evictAll();                Log.d("CacheUtils", "mMemoryCache.size()" + mMemoryCache.size());            }            mMemoryCache = null;        }    }    public Bitmap loadLocal(String path) {        Bitmap bitmap=BitmapFactory.decodeFile(path);        addBitmapToMemoryCache(path, bitmap);            return getBitmapFromMemCache(path);    }    public void clearCache() {        if (mMemoryCache != null) {            if (mMemoryCache.size() > 0) {                Log.d("CacheUtils",                        "mMemoryCache.size() " + mMemoryCache.size());                mMemoryCache.evictAll();                Log.d("CacheUtils", "mMemoryCache.size()" + mMemoryCache.size());            }            mMemoryCache = null;        }    }    /*    将图片进行压缩    BitmapFactory.Options options = new BitmapFactory.Options();    options.inJustDecodeBounds = true; // 设置了此属性一定要记得将值设置为false    Bitmap bitmap = null;    bitmap = BitmapFactory.decodeFile(url, options);    int be = (int) ((options.outHeight > options.outWidth ? options.outHeight / 150            : options.outWidth / 200));    if (be <= 0) // 判断200是否超过原始图片高度    be = 1; // 如果超过,则不进行缩放    options.inSampleSize = be;    options.inPreferredConfig = Bitmap.Config.ARGB_4444;    options.inPurgeable = true;    options.inInputShareable = true;    options.inJustDecodeBounds = false;    try {        bitmap = BitmapFactory.decodeFile(url, options);    } catch (OutOfMemoryError e) {        System.gc();        Log.e(TAG, "OutOfMemoryError");    }    */}

结语

通过上面的分析,我们了解到LruCache是通过LinkedHashMap来实现,使用LRU算法。

LruCache是对LRU策略的内存缓存的实现,后来的系统源码中也曾经加上该算法的磁盘缓存的实现,也有对应磁盘缓存的源码DiskLruCache.Java。有兴趣的可以自己去看一下。

本篇文章是个人的理解,如有错误请指出。欢迎大家一起交流!

6 0
原创粉丝点击