Android内存缓存管理LruCache源码解析与示例
2017-04-21 15:16
871 查看
在深圳出差,非常忙,抽空写文章,这些文章的质量很可能不高,但还是希望可以帮到你。
在加载图片的时候,我们要考虑到内存问题,(内存缓存作为最先被读取的数据,应该存储那些经常使用的数据对象,且内存容量有限,内存缓存的容量应该限定。)如果你加载是高清无码大图很可能会造成OOM,那我们需要一个东西来管理这个图片与其缓存。
问题:当有新的内容需要加入我们的缓存,但我们的缓存空闲的空间不足以放进新的内容时,如何舍弃原有的部分内容从而腾出空间用来放新的内容。
明显的可以看出LruCache是利用了LinkedHashMap
走到这里我们有个疑问—LinkedHashMap是什么?它是怎么实现LRU这种缓存策略的?
看文中的一句代码:
参数说明:
initialCapacity 初始容量大小,使用无参构造方法时,此值默认是4(安卓SdkVersion 24中默认4,这里是使用了父类HashMap的默认值)
loadFactor 加载因子,使用无参构造方法时,此值默认是 0.75f(安卓SdkVersion 24中默认0.75,这里是使用了父类HashMap的默认值)
accessOrder false: 基于插入顺序 true: 基于访问顺序
LinkedHashMap继承自HashMap,不同的是,它是一个双向循环链表,它的每一个数据结点都有两个指针,分别指向直接前驱和直接后继,这一个我们可以从它的内部类LinkedEntry中看出,其定义如下:
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)来赋值。
好,我们来看一下父类的添加方法:
想要理解HashMap可以看我的这篇HashMap源码解析
当我们加入新的元素之后,链表的顺序如图:
进入recordAccess()查看:
这个方法是在LinkedHashMapEntry内部:
图一:
是不是理解了LinkedHashMap的排序原理了?
所以LruCache定义了以下三个必需的成员变量:
LruCache是可能被多个线程同时访问的,所以在读写map时进行加锁。
当获取不到对应的key的值时,它会调用其create(K key)方法,这个方法用于当缓存没有命名时计算一个key所对应的值,它的默认实现是直接返回null。
这个方法并没有加上同步锁,也就是在它进行创建时,map可能已经有了变化。
所以在get方法中,如果create(key)返回的V不为null,会再把它给放到map中,并检查是否在它创建的期间已经有其他对象也进行创建并放到map中了,
如果有,则会放弃这个创建的对象,而把之前的对象留下,否则因为我们放入了新创建的值,所以要计算现在的大小并进行trimToSize。
trimToSize方法是根据传进来的maxSize,如果当前大小超过了这个maxSize,则会移除最老的结点,直到不超过。
主要逻辑是,计算新增加的大小,加入size,
然后把key-value放入map中,如果是更新旧的数据(map.put(key, value)会返回之前的value),则减去旧数据的大小,并调用entryRemoved(false, key, previous, value)方法通知旧数据被更新为新的值。
最后也是调用trimToSize(maxSize)修整缓存的大小。
LruCache是对LRU策略的内存缓存的实现,后来的系统源码中也曾经加上该算法的磁盘缓存的实现,也有对应磁盘缓存的源码DiskLruCache.Java。有兴趣的可以自己去看一下。
本篇文章是个人的理解,如有错误请指出。欢迎大家一起交流!
在加载图片的时候,我们要考虑到内存问题,(内存缓存作为最先被读取的数据,应该存储那些经常使用的数据对象,且内存容量有限,内存缓存的容量应该限定。)如果你加载是高清无码大图很可能会造成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。有兴趣的可以自己去看一下。
本篇文章是个人的理解,如有错误请指出。欢迎大家一起交流!
相关文章推荐
- android LruCache内存缓存源码解析
- Android内存缓存LruCache源码解析
- android lru缓存 辅助类LruCache源码解析
- Android 源码解析-LruCache 缓存工具类
- 内存缓存LruCache源码解析
- android网络相册(带磁盘缓存DiskLruCache 和内存缓存LruCache)
- Android DiskLruCache 源码解析 硬盘缓存的绝佳方案
- Android DiskLruCache缓存完全解析
- Android 图片缓冲的管理-内存缓存
- android之LruCache源码解析
- Android DiskLruCache 源码解析 硬盘缓存的绝佳方案
- redis源码解析之内存管理
- android 提供的内存缓存LruCache.java
- android图片缓存之内存缓存技术LruCache,软引用
- Android内存缓存LruCache
- Android DiskLruCache 源码解析 硬盘缓存的绝佳方案
- Android 图片缓存之内存缓存技术LruCache,软引用
- android LruCache的使用 (本地缓存+内存缓存)
- Android 图片缓存之内存缓存技术LruCache,软引用
- [Android源码解析] 清空应用内部文件缓存