概述
图片的加载一直是Android开发中重要的一环,处理不当的话很容易就会造成OOM,尤其是当我们需要使用大量图片的时候,图片的缓存技术就至关重要了。
图片的缓存技术分为三级:
1.内存缓存 : 读取速度最快
技术: LruCache
2.本地缓存 : 读取速度低于内存缓存
技术: DiskLruCache
3.网络缓存 : 读取速度最慢
技术: 保存在服务器中
图片的缓存流程: 图片需要从缓存中获取的时候,先从内存缓存中获取,如果没有的话,再从本地缓存中获取,如果还没有的话,就只能从网络中去获取。
本文主要讲解LruCache的实现和原理。
为什么会出现LruCache?
由于 Android 为每个进程分配的可用内存都是有限的,如果进程使用的内存超过了所分配的限制就会出现内存溢出问题。同时,如果应用中每个资源都需要从本地或网络加载,这会影响到应用的性能,为了保证应用性能又避免内存溢出,于是出现内存缓存技术。
LruCache的优点
是为了应用能够更有效的去管理内存,保证了缓存是处于一种可控的状态,有效的防止了OOM的出现。
使用场景
一般是使用于图片的缓存,缓存技术有助于减少图片的网络请求,也有助于用户体验。
基本使用
初始化:
int maxMemory = (int) (Runtime.getRuntime().totalMemory() / 1024);
int cacheSize = maxMemory / 8;
LruCache<String,Bitmap> cache = new LruCache<String,Bitmap>(cacheSize) {
// 计算当前的Bitmap的内存大小,默认是返回 1。
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount() / 1024;
}
// 如果您的缓存值包含需要显式释放的资源,请重写此方法
@Override
protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
super.entryRemoved(evicted, key, oldValue, newValue);
}
};
put方法:
cache.put("key", bitmap);
get方法:
cache.get("key");
remove方法:
cache.remove("key");
缓存原理
其实就是维护了一个缓存对象列表,将没有访问过的对象都放在队尾,将最近添加和最近使用过的对象放在队头,当需要释放缓存的时候,从队尾开始剔除对象。

源码分析
获取对象
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); // 保存需要缓存的对象
}
可以从它的构造参数看出,是通过LinkedHashMap 来保存需要缓存的数据。
为什么要用LinkedHashMap来维持缓存呢?
LinkedHashMap 是由数组 加 双向链表的数据结构来实现。 其中的双向链表的结构可以实现 访问顺序 和 插入顺序。
LinkedHashMap 的构造参数:
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
其中 accessOrder 设置为true是访问顺序,false为插入顺序。
让我们来看一个例子:
public static void main(String[] args) {
LinkedHashMap<Integer, Integer> map = new LinkedHashMap<>(0,0.75f,true);
map.put(0, 0);
map.put(1, 1);
map.put(2, 2);
map.put(3, 3);
map.put(4, 4);
map.put(5, 5);
map.put(6, 6);
map.get(1);
map.get(2);
for(Map.Entry<Integer, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + ":" + entry.getValue());
}
}
结果:
0:0
3:3
4:4
5:5
6:6
1:1
2:2
可以看到,最近使用的 1 和 2 都放在了表头。
这就满足了LRU缓存算法的思想,所以LinkedHashMap会被LruCache使用。
现在让我们来具体看看LruCache的put、get、remove方法。
put方法
会将添加的对象放在队列的第一个位置
public final V put(K key, V value) {
if (key == null || value == null) {
throw new NullPointerException("key == null || value == null");
}
V previous; // 如果map中存在相同的数据,那么这个就是map中那个相同的数据
synchronized (this) {
putCount++; // 添加的数量加一
size += safeSizeOf(key, value); // 将内存的大小增大
previous = map.put(key, value); // 将 新的数据添加到map的头部中,如果map中存在,那么旧数据会被移除,也就是previous。
if (previous != null) { // 旧数据移除,就需要减去旧数据的大小
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
entryRemoved(false, key, previous, value);
}
trimToSize(maxSize);
return previous;
}
让我们来看看 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;
}
再来看看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) {
break;
}
// Map.Entry<K, V> toEvict = map.entrySet().iterator().next()
// 这是取出队尾的元素
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);
}
}
可以看到代码中,循环的去删除多出来的对象,直到当前的缓存大小小于或者等于设定大小。
再来看看entryRemoved方法:
这个方法使用的时候需要被重写,先来看看他的四个参数的含义:
evicted
true 表示 数据是 trimToSize 移除的,为了释放空间, false 表示 是 put 和remove 主动移除的
key
就是map的key
oldValue
被移除的旧的数据
newValue
保存的新的数据,如果没有,则是null
get方法
public final V get(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
// map的get方法会使得map中的元素移动到头部
mapValue = map.get(key);
if (mapValue != null) {
hitCount++;
return mapValue;
}
missCount++;
}
/**
如果缓存取不到数据的话,可以创建一个数据,create()方法默认返回null
需要对它进行重写
*/
V createdValue = create(key);
if (createdValue == null) {
return null;
}
synchronized (this) {
createCount++;
mapValue = map.put(key, createdValue);
// 当 map中存在创建的数据的时候,将旧数据重新的put到map的表头中。
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;
}
}
get方法使用的时候,会将被使用的对象移动到 队列的表头。
remove方法
public final V remove(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V previous;
synchronized (this) {
// 在map中移除,并减少 缓存的大小
previous = map.remove(key);
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
entryRemoved(false, key, previous, null);
}
return previous;
}
结语
以上便是LruCache的原理,它的思想也是非常容易理解的,LruCache中的关键在于LinkedHashMap,如果有时间的话,可以去看看LinkedHashMap的实现。