Android
Android LruCache实现分析
LruCache 是安卓开发中常用到的缓存技术,LRU 的全名是 Least Recently Used,表示最近最少使用算法,也就是说当内存快到达阈值时,若某个对象最近很少使用的,那么它就会被回收掉以释放内存。
使用
LruCache 的使用方法大致如下 :
// LruCache 的声明初始化
int maxMemory = ( int ) ( Runtime . getRuntime (). maxMemory () / 1024 );
// 指定缓存的内存大小,一般为当前进程可用内存的 1/8 即可
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 ;
}
// 当缓存对象被回收时执行的回调方法,以便释放一些资源之类的,默认为空
@Override
protected void entryRemoved ( boolean evicted , String key , Bitmap oldValue , Bitmap newValue ) {
super . entryRemoved ( evicted , key , oldValue , newValue );
}
};
// LruCache 添加缓存对象
cache . put ( key , bitmap ) ;
// LruCache 获取缓存对象
cache . get ( key );
LruCache 的声明和使用都比较简单,在它的内部就已经帮我们完成了缓存对象的移除和添加,而我们无需过多的考虑对象的释放问题。
原理分析
既然要缓存对象,必然要有个容器,LruCache 使用的是 LinkedHashMap
,以强引用方式存储外界的缓存对象。
LinkedHashMap
是一个双向的环形链表,链表中的每个节点都有指向前一个元素和后一个元素的引用。使用链表的好处就在于它的删除和添加操作更加的方便快捷。
而 LRU 的最近最少使用算法思想就体现在LinkedHashMap
的元素顺序中了。
LinkedHashMap
中的顺序当然是从头到尾的,而一个元素最近的使用情况当然也是从头到尾的一个渐变了。在 LruCache 中,默认是最近最少使用的元素位于链表的头部,而最近使用最多的元素则是位于链表的尾部。这样一来,从头到尾的一个渐变过程也就是一个元素使用最少到多的过程了。
当我们在进行每个get
和put
操作时,LinkedHashMap
中的每个元素使用情况就要发生变化了,毕竟缓存容量是有限的,一些不幸的元素可能就因此而被垃圾回收了,以此来体现 LRU 的最近最少使用算法。
下面就分别介绍其get
和put
操作。
LruCache 的 get 操作
从 LruCache 中取出一个元素,显然,这个结果是可能为 null 的。
LruCache 使用get
操作很简单,有就取出来,没有就返回个 null 。
V mapValue ;
synchronized ( this ) {
mapValue = map . get ( key );
if ( mapValue != null ) {
hitCount ++ ;
return mapValue ;
}
missCount ++ ;
}
而重点就在于LinkedHashMap
的get
方法了。
int hash = Collections . secondaryHash ( key ); // 计算 key 的 hash 值
HashMapEntry < K , V >[] tab = table ;
for ( HashMapEntry < K , V > e = tab [ hash & ( tab . length - 1 ) ] ; e != null ; e = e . next ) {
K eKey = e . key ;
if ( eKey == key || ( e . hash == hash && key . equals ( eKey ))) {
if ( accessOrder )
makeTail (( LinkedEntry < K , V > ) e );
return e . value ;
}
}
LinkedHashMap
的节点类是LinkedEntry
,它继承自HashMapEntry
,在其继承之上多了指向前后元素的引用。
所以,从上述代码可以看出,无非就是一个遍历,找到 key
对应的元素就取出来,并且多了一个操作makeTail
。
makeTail
的操作又是干什么的呢?它可谓是实现最近最少算法的精髓了。
private void makeTail ( LinkedEntry < K , V > e ) {
// Unlink e 将节点 e 的前节点和后节点进行连接,而将它自己给释放出来
e . prv . nxt = e . nxt ;
e . nxt . prv = e . prv ;
// Relink e as tail 将节点 e 连接到链表的尾部
LinkedEntry < K , V > header = this . header ;
LinkedEntry < K , V > oldTail = header . prv ;
e . nxt = header ;
e . prv = oldTail ;
oldTail . nxt = header . prv = e ;
modCount ++ ;
}
makeTail
操作,将一个get
的节点,从原本它在链表中的位置移到了整个链表中的尾部,使它的前节点是上一任的尾节点,而后节点则是头结点。
而在上面提到过,LinkedHashMap
链表中元素的顺序正是完美的体现了最少最近算法的思想。
get
得到的元素必然是最近使用最多的了,每次 get
之后都将它移动到链表的尾部,也正是最近最少算法的体现了。
LruCache 的 put 操作
put
操作则是向LinkedHashMap
中放入一个元素,而缓存的容量又是有限的,这么一来,在有限的情况下,就会有元素被垃圾回收了。
V previous ;
synchronized ( this ) {
putCount ++ ;
size += safeSizeOf ( key , value );
previous = map . put ( key , value ); // 若存在 key 相同的元素,则返回之前的,否则返回 null
if ( previous != null ) { // 存在 key 相同的元素
size -= safeSizeOf ( key , previous );
}
}
// 移除最近最少使用的元素
trimToSize ( maxSize );
当进行put
操作时,若放入一个 key 存在的元素,则返回已经存在的对应的值。
同时,在每次put
操作之后,都要进行容量的清除工作。
public void trimToSize ( int maxSize ) {
while ( true ) { // 死循环
K key ;
V value ;
synchronized ( this ) {
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 );
}
}
trimToSize
的操作是一个死循环的操作,当size
小于maxSize
时,直接返回,否则从LinkedHashMap
中移除最近最少使用的元素,也就是头节点后的元素。
以上就大致实现了 LRU 的最少最近算法。
同步锁相关操作
在 LruCache 的get
和put
操作中,都有用到的synchronized
锁操作。
这是保证线程安全,因为在get
和put
方法中进行的都是复合操作,在并发条件下使用缓存时很容易出现脏数据,加上synchronized
标识,则保证了操作的原子性,使得成为线程安全的操作。
参考
1、https://github.com/LittleFriendsGroup/AndroidSdkSourceAnalysis/blob/master/article/LruCache%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90.md