对于LruCache的分析,我们可以从官方介绍开始,直接把源码中的注释拿过来。
A cache that holds strong references to a limited number of values. Each time a value is accessed, it is moved to the head of a queue. When a value is added to a full cache, the value at the end of that queue is evicted and may become eligible for garbage collection.
简单翻译一下:首先用户缓存,它持有有限数量的value的强引用。每当某个value被取用时,他都会被移到队头。当某个value要被加入到一个已经满了的cache当中时,位于队尾的value将会被移除,并且能够被gc给回收到。
要分析一个类,我们主要看他的属性和方法,当然还有它所继承的接口和父类。
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;
private int putCount;
private int createCount;
private int evictionCount;
private int hitCount;
private int missCount;
1.
其中最重要的也是LruCache的核心就是这个LinkedHashMap,如果对数据结构比较熟悉的同学应该知道它的底层逻辑了。这里简单说明一下:
LinkedHashMap继承自HashMap,所以他的存储数据的方式首先是<k,v>的方式,也就是键值对的方式。那它与HashMap有哪些不同点呢,首先就体现在Linked这个词上,Linked指的是LinkedList,也就是说它是有序的,而我们知道hashmap是无序的。也就是说,通过引入LinkedList使得LinkedHashMap可以排序。熟悉LinkedHashMap应该知道它有两种排序方式,一种是插入排序,一种是读取排序。LruCache正是巧妙的运用的LinkedHashMap的读取排序,来实现我们说的Lru算法(最近最少使用算法)。
2.
这两个单位要统一,比如我们常用LruCache来做图片内存缓存,图片的文件大小1M可以用来表示size单元。maxSize则表示最大的size。比如我们LruCache最多缓存50M,那么MaxSize就是50,而size则是value.getByteCount()/1024.
具体实现:
long maxMemory = Runtime.getRuntime().maxMemory();
int cacheSize = (int) (maxMemory / 8);//可用内存的八分之一
LruCache lruCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount();
}
};
3.下面那几个count用得不多,所以这里也就不多做介绍。
下面我们再看看几个重要方法:
- 构造函数。
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);
}
构造函数做了两件事,
- 定义了最大存储size,也就是我们上面分析的maxSize,是size的整数倍。
- 初始化了核心的LinkedHashMap,而且是用了三个参数的构造函数,第三个参数为true,也就是使用了读取排序。
可以瞄一眼LinkedHashMap的构造函数,第三个用于表示是否要使用accessOrder。true是读取排序,而false的话则会采用插入排序。
/**
* Constructs an empty <tt>LinkedHashMap</tt> instance with the
* specified initial capacity, load factor and ordering mode.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
* @param accessOrder the ordering mode - <tt>true</tt> for
* access-order, <tt>false</tt> for insertion-order
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
- resize,重新给定maxSize
public void resize(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
synchronized (this) {
this.maxSize = maxSize;
}
trimToSize(maxSize);
}
因为重新给定的maxSize可能要小于之前的size,所以就需要判断是否需要移除末尾的某些value来使得满足条件。
- trimToSize,移除最近最少使用的元素以致size小于maxSize。
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.eldest();
if (toEvict == null) {
break;
}
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
size -= safeSizeOf(key, value);
evictionCount++;
}
entryRemoved(true, key, value, null);
}
}
当某个value被移除时,会调用entryRemoved
- entryRemoved 方法,如果你cache的某个值需要明确释放,重写entryRemoved()方法。这个方法会在元素被put或remove时调用,源码默认是空实现的。
protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}
- 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;
}
这里需要注意的是,LruCache不允许key或者value为空。当添加某个value进缓存实际上就是将其添加的map容器中,如果之前存在这个key对应的value,那么还会调用以下方法:
1.减少size 因为之前的value被移除而且能被垃圾回收了
2.调用entryRemoved方法
3.trimToSize 因为可能size超过了maxSize
4.返回previous
- 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++;
return mapValue;
}
missCount++;
}
/*
* Attempt to create a value. This may take a long time, and the map
* may be different when create() returns. If a conflicting value was
* added to the map while create() was working, we leave that value in
* the map and release the created value.
*/
V createdValue = create(key);
if (createdValue == null) {
return null;
}
synchronized (this) {
createCount++;
mapValue = map.put(key, createdValue);
if (mapValue != null) {
// There was a conflict so undo that last put
map.put(key, mapValue);
} else {
size += safeSizeOf(key, createdValue);
}
}
if (mapValue != null) {
entryRemoved(false, key, createdValue, mapValue);
return mapValue;
} else {
trimToSize(maxSize);
return createdValue;
}
}
正常情况下,其实就是从map中取key对应的value。还有一种情况,如果我们重写了create方法,那么情况就不一样了。
会将create的值先存入map,然后判断是否之前key已经对应有值了,有的话再将之前的值替换当前新创建的值并调用entryRemoved函数,没有的话则需要重新赋值size并调用trimToSize。
其他的方法就不太常用了,在这里就不讨论了。
讲到这里,就不得不提一下,如果让你实现一个图片加载框架的话,你会怎么实现呢?
可以后续分析以下glide的源码
网友评论