碼噠,今天無意中發現Android 5.0(api level 21)之前的LruCache實現居然存在一個bug.
由於在電腦上(Java SE環境)測試code比較方便,我便將最近寫在Android項目中的框架代碼copy到Java項目中進行測試,然後缺少一個LruCache, 我也直接將其源碼copy過來,但是報了一個錯誤,正是下面第一段代碼中map.eldest();
這句,但是這個方法由於不是Java標準API而被@hide
了,我便想都不想,直接改成了遍歷map並取出最後一個元素(思維定式,以爲LinkedHashMap的最後一個元素就是那個eldest()的,即LRU(Least Recently Used)的),但是測試中很快發現了問題,然後在LinkedHashMap源碼中找到其定義及註釋,修改爲取出鏈表的第一個元素了。
然而湊巧的是,今天下班我把代碼copy到自己的本上帶回家測試,再次copy這個LruCache源碼的時候,發現居然沒有報錯,納悶中,我去翻看那行怎麼不報錯,便發現了該問題。那段代碼正好跟我昨天剛開始犯的錯誤一樣,遍歷最後一個元素當做LRU的,並將去驅逐。並且加了註釋,大意是說:由於map.eldest();
爲非標準API, 所以將其修改了。
OK,看代碼吧。
####首先來看看正確的實現 Android 6.0(API Level 23):
/**
* Remove the eldest entries until the total of remaining entries is at or
* below the requested size.
*
* @param maxSize the maximum size of the cache before returning. May be -1
* to evict even 0-sized elements.
*/
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);
}
}
其中有個map.eldest();
方法,表示取出LinkedHashMap中最年長的元素,並將其驅逐。
####下面來看看錯誤的實現 Android 5.0(API Level 21):
/**
* @param maxSize the maximum size of the cache before returning. May be -1
* to evict even 0-sized elements.
*/
private 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;
}
// BEGIN LAYOUTLIB CHANGE
// get the last item in the linked list.
// This is not efficient, the goal here is to minimize the changes
// compared to the platform version.
Map.Entry<K, V> toEvict = null;
for (Map.Entry<K, V> 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<K, V> toEvict = null;
for (Map.Entry<K, V> 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
* <p>
* 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
.
#####然而這段代碼經過測試,在Cache Size填滿後,確實總是驅逐最後添加進去的元素。顯然不符合Lru的意圖。
需要注意的是,LruCache的map是這麼構造的:this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
,重點在這個true
, 表示任何一個操作(get, put等)都將觸發重排序,將這個被操作的元素排到鏈表的末尾,因此末尾的是最近“頻繁”使用的,而不是LRU的,那麼這個取出末尾的那個元素並將其驅逐的邏輯,顯然是錯誤的!
####那麼我們還是回過頭來看看eldest()
到底做了什麼吧!
/**
* Returns the eldest entry in the map, or {@code null} if the map is empty.
* @hide
*/
public Entry<K, V> eldest() {
LinkedEntry<K, V> eldest = header.nxt;
return eldest != header ? eldest : null;
}
eldest()
直接返回了header.nxt
.
public class LinkedHashMap<K, V> extends HashMap<K, V> {
/**
* 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<K, V> header;
而header.nxt
又是The first real entry
. 意思很明確了,就是返回鏈表的第一個元素。
####那麼可以肯定Android 5.0(API Level 21)及以前對LruCache的實現就是一個bug了。