代码,今天无意中发现Android5.0(APIlevel21 )以前的LruCache实现有错误。
因为在电脑(Java SE环境、手机以外)上测试代码很方便,所以最近在Java项目上测试了Android项目的框架代码copy,缺了一个LruCache。 我也直接复制了那个源代码,但是报告了错误。 那是下一个第一个代码的map.eldest ); 虽然是这个语句,但是因为这个方法不是Java标准API,所以被@hide了。 我也没想,就这样遍历map取出最后的要素(改为思考例程)。 LinkedHashMap的最后一个元素是其eldest ) )的,也就是LRU的),但在测试中很快发现了问题,LinkedHashMap的
但是,不凑巧的是,今天下班了,把代码copy带回到自己身边准备测试。 当我再次查询名为copy的LruCache源代码时,竟然不报告错误,在我还没明白的时候,我又回到了那一行,发现了问题。 那个代码正好和我昨天犯的错误一样,调查最后的要素作为LRU,驱逐。 然后,单击“map.eldest ()”; 因为是非标准API,所以修正了。
好的,看代码吧。
首先,我们来看看正确的Android6.0(APIlevel23 )实现。
//*
* removetheeldestentriesuntilthetotalofremainingentriesisator
* below the要求的大小。
*
* @ parammaxsizethemaximumsizeofthecachebeforereturning.maybe-1
* to evict even0-大小写元素。
*/
公共限制(int maxsize )。
while (真)。
key;
V value;
同步(this ) {
if(size0||(map.isempty ) ) size!=0) }
thrownewillegalstateexception (getclass ().getName ) )。
'.sizeof (isreportinginconsistentresults!' );
}
if(size=maxsize ) {
黑;
}
Map.Entry toEvict=map.eldest (;
if (to evict==空值) {
黑;
}
key=toEvict.getKey (;
value=toEvict.getValue (;
map.remove(key;
size-=safesizeof(key,value );
evictionCount;
}
实体移除(true,key,value,null );
}
}
其中包含map.eldest (; 方法表示取出LinkedHashMap中最年长的元素并驱逐。
让我们看看错误的实现Android5.0(APIlevel21 )。
//*
* @ parammaxsizethemaximumsizeofthecachebeforereturning.maybe-1
* to evict even0-大小写元素。
*/
权限限制(int maxsize )。
while (真)。
key;
V value;
同步(this ) {
if(size0||(map.isempty ) ) size!=0) }
thrownewillegalstateexception (getclass ().getName ) )。
'.sizeof (isreportinginconsistentresults!' );
}
if(size=maxsize ) {
黑;
}
//BEGIN LAYOUTLIB CHANGE
//getthelastiteminthelinkedlist。
//This is not ef
ficient, the goal here is to minimize the changes// compared to the platform version.
Map.Entry toEvict = null;
for (Map.Entry entry : map.entrySet()) {
toEvict = entry;
}
// END LAYOUTLIB CHANGE
if (toEvict == null) {
break;
}
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
size -= safeSizeOf(key, value);
evictionCount++;
}
entryRemoved(true, key, value, null);
}
}
注意其中这段以及其注释说明:
Map.Entry toEvict = null;
for (Map.Entry entry : map.entrySet()) {
toEvict = entry;
}
遍历取出最后一个元素,这个正是将被驱逐的元素。同时在类说明中给了一段注释:
import java.util.LinkedHashMap;
import java.util.Map;
/**
* BEGIN LAYOUTLIB CHANGE
* This is a custom version that doesn't use the non standard LinkedHashMap#eldest.
* END LAYOUTLIB CHANGE
*
* 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
* ...(略)
是说,这个版本不能使用非标准APILinkedHashMap#eldest。然而紧接着的一句话:
Each time a value is accessed, it is moved to the head of a queue.
每次访问一个值时,它都会被移动到队列的head(意思是说head是 most-recently,不是 least-recently)。
就说错了,有LinkedHashMap的文档为证:
* A special {@link #LinkedHashMap(int,float,boolean) constructor} is
* provided to create a linked hash map whose order of iteration is the order
* in which its entries were last accessed, from least-recently accessed to
* most-recently (access-order). This kind of map is well-suited to
* building LRU caches.
从“较远的近”(最近最少访问)到“极为最近”排序,说明head是“较远的近”,是 least-recently,是eldest。
这段代码经过测试,在Cache Size填满后,确实总是驱逐最后添加进去的元素。显然不符合Lru的意图。
需要注意的是,LruCache的map是这么构造的:this.map = new LinkedHashMap(0, 0.75f, true);,重点在这个true, 表示任何一个操作(get, put等)都将触发重排序,将这个被操作的元素排到链表的末尾,因此末尾的是最近“频繁”使用的,而不是 LRU(Least Recently Used)最近“最少”使用的,那么这个取出末尾的那个元素并将其驱逐的逻辑,显然是错误的!
那么我们还是回过头来看看eldest()到底做了什么吧!
/**
* Returns the eldest entry in the map, or {@code null} if the map is empty.
* @hide
*/
public Entry eldest() {
LinkedEntry eldest = header.nxt;
return eldest != header ? eldest : null;
}
eldest()直接返回了header.nxt.
public class LinkedHashMap extends HashMap {
/**
* A dummy entry in the circular linked list of entries in the map.
* The first real entry is header.nxt, and the last is header.prv.
* If the map is empty, header.nxt == header && header.prv == header.
*/
transient LinkedEntry header;
而header.nxt又是The first real entry. 意思很明确了,就是返回链表的第一个元素。
那么可以肯定Android 5.0(API Level 21)及以前对LruCache的实现就是一个bug了。