Android LruCache的bug 頂 原 薦

碼噠,今天無意中發現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了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章