简介
为什么用
一个app持有的内存是有限的,无限制的使用强引用在内存中缓存数据,有可能导致OOM。
能做什么
- LruCache会创建一个固定大小的缓存池,并维持一个LinkHashMap来有序的缓存数据。
- 在往缓存池put或get数据的时候,LinkHashMap会将最近使用的数据移动到队尾。
- 在往缓存池put数据的时候,LruCache会计算当前已缓存数据的大小。如果当前缓存数据超过了限定值,LruCache会将LinkHashMap队首的数据删除,直到缓存数据大小满足限定值。
即缓存一定量的数据,并在缓存数据超限时删除长期不用的数据。
如何使用
以缓存Bitmap为例:
int maxMemory = (int) (Runtime.getRuntime().totalMemory() / 1024);
int cacheSize = maxMemory / 8;
LruCache<String, Bitmap> cache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight() / 1024;
}
};
需要缓存图片时,直接lruCache.put(key, bitmap);
需要获取图片时,三级缓存,先从内存拿,故调用lruCache.get从内存缓存拿。
源码分析
前提知识
LinkHashMap可以将最近使用的元素移动至队尾。见构造函数:
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
//LruCache中如下初始化LinkHashMap
new LinkedHashMap<K, V>(0, 0.75f, true)
第三个参数:accessOrder为true时,在LinkHashMap调用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;
}
排序会将get的元素移动到LinkHashMap的队尾:
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMapEntry<K,V> last;
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;
}
}
LruCache源码分析
构造函数
首先是构造函数,LruCache的构造函数指定了缓存的maxSize,以及创建了一个get会自动排序的LinkHashMap。
public LruCache(int maxSize) {
...
this.maxSize = maxSize;
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}
这里需要注意的是:maxSize是可定制的,不一定是占用内存的大小,也可以是元素的数量(数量也可以限制缓存的大小)。
如何鉴别maxSize的意义呢?
上面使用LruCache的例子中,重写了一个方法sizeOf:
LruCache<String, Bitmap> cache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight() / 1024;
}
};
sizeOf是返回当前元素占用maxSize的大小(不是比例),它的默认方法如下:
protected int sizeOf(K key, V value) {
return 1;
}
默认返回1,表示占用一个元素。可见LruCache的maxSize的默认意义是可容纳元素的数量。
上述例子中,重写了sizeOf,使其返回bitmap的大小,故maxSize的意义也应当变更为可缓存数据的内存大小。
Put
分析完构造函数,接下来就是LruCache的功能函数:put和get
先存后取,先分析put。put的源码如下:
public final V put(K key, V value) {
...
V previous;
synchronized (this) {
...
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;
}
这个方法做了三件事:
1.将数据存储到LinkHashMap里。previous是该key对应的旧value,没有则返回null。
previous = map.put(key, value);
2.重新计算当前size。size = size + 新value_size - 旧value_size。这里加了同步,防止多线程size计算错误。
size += safeSizeOf(key, value);
...
if (previous != null) {
size -= safeSizeOf(key, previous);
}
safeSizeOf()就是上面重写的sizeOf(),只不过加了一层size<0的异常处理。
3.计算当前size<maxSize?,不满足则删除LinkHashMap队首元素,删除后重新计算size直到<maxSize。
trimToSize(maxSize);
trimToSize(maxSize)的源码如下:
public void trimToSize(int maxSize) {
while (true) {
K key;
V value;
synchronized (this) {
...
//size<maxSize,退出删除循环;否则继续删除
if (size <= maxSize) {
break;
}
//获取队首元素,也就是最近最少使用的元素
Map.Entry<K, V> toEvict = map.eldest();
...
//remove队首元素,并重新计算size
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
size -= safeSizeOf(key, value);
...
}
entryRemoved(true, key, value, null);
}
}
参考注释
无论是
put()
,还是trimToSize()
,似乎都有调用一个方法entryRemoved()
。
entryRemove()
是LruCache的一个空方法,设置它的意义在于:LruCache之所以能缓存,其实就是利用LinkHashMap对数据强引用。删除缓存,实际就是解除强引用。但是对于有些数据,强引用的解除并不能真正的回收。如旧版本的Android,对于Bitmap的回收必须手动调用recycle()。这个方法,就是提供给外部,以满足类似这种额外的内存回收需求。
Get
Get的源码如下:
public final V get(K key) {
...
//获取数据
V mapValue;
synchronized (this) {
mapValue = map.get(key);
if (mapValue != null) {
...
return mapValue;
}
...
}
//获取不到,尝试create数据
V createdValue = create(key);
if (createdValue == null) {
return null;
}
synchronized (this) {
createCount++;
mapValue = map.put(key, createdValue);
if (mapValue != null) {
map.put(key, mapValue);
} else {
size += safeSizeOf(key, createdValue);
}
}
if (mapValue != null) {
entryRemoved(false, key, createdValue, mapValue);
return mapValue;
} else {
trimToSize(maxSize);
return createdValue;
}
}
这个方法做了俩件事:
1.将数据从LinkHashMap中取出并返回。(排序是LinkHashMap在get时调用的,LruCache无须关心)
mapValue = map.get(key);
if (mapValue != null) {
...
return mapValue;
}
2.如果获取不到数据,尝试create。create的默认方法如下:
protected V create(K key) {
return null;
}
同样,create()
类似于entryRemoved()
,也是提供给外部,供其在获取不到数据下提供默认数据的方法。
下节将分析DiskLruCache。
网友评论