LruCache 使用及原理

ZedeChan發表於2018-07-29

1. LruCache 是什麼?

要搞清楚 LruCache 是什麼之前,首先要知道 Android 的快取策略。其實快取策略很簡單,舉個例子,就是使用者第一次使用網路載入一張圖片後,下次載入這張圖片的時候,並不會從網路載入,而是會從記憶體或者硬碟載入這張圖片。

快取策略分為新增、獲取和刪除,為什麼需要刪除快取呢?因為每個裝置都會有一定的容量限制,當容量滿了的話就需要刪除。

那什麼是 LruCache 呢?其實 LRU(Least Recently Used) 的意思就是近期最少使用演算法,它的核心思想就是會優先淘汰那些近期最少使用的快取物件。

2. LruCache 怎麼用?

現在使用 okhttp 載入網上的一張圖片:

新建一個 ImageLoader 類:

public class ImageLoader {

    private LruCache<String, Bitmap> lruCache;

    public ImageLoader() {
        int maxMemory = (int)(Runtime.getRuntime().maxMemory() / 1024);
        int cacheSize = maxMemory / 8;
        lruCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
            }
        };
    }

    public void addBitmap(String key, Bitmap bitmap) {
        if (getBitmap(key) == null) {
            lruCache.put(key, bitmap);
        }
    }

    public Bitmap getBitmap(String key) {
        return lruCache.get(key);
    }

}
複製程式碼

重寫 sizeOf() 就是來計算一個元素的快取的大小的,當存放的元素的總快取大小大於 cacheSize 的話,LruCache 就會刪除最近最少使用的元素。

MainActivity:

public class MainActivity extends AppCompatActivity {

    private String Path = "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1532852517262&di=bcbc367241183c39d6e6c9ea2f879166&imgtype=0&src=http%3A%2F%2Fimg4q.duitang.com%2Fuploads%2Fitem%2F201409%2F07%2F20140907002919_eCXPM.jpeg";

    private Button btn;

    private ImageView imageView;

    private ImageLoader imageLoader;

    private static final int SUCCESS = 1;
    private static final int FAIL = 2;

    Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case SUCCESS:
                    byte[] Picture = (byte[]) msg.obj;
                    Bitmap bitmap = BitmapFactory.decodeByteArray(Picture, 0, Picture.length);
                    imageLoader.addBitmap("bitmap", bitmap);
                    imageView.setImageBitmap(bitmap);

                    break;
                case FAIL:
                    break;
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        btn = findViewById(R.id.btn);
        imageView = findViewById(R.id.imageview);
        imageLoader = new ImageLoader();
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Bitmap bitmap = getBitmapFromCache();
                if(bitmap != null) {
                    imageView.setImageBitmap(bitmap);
                } else {
                    getBitmapFromInternet();
                }
            }
        });

    }

    private Bitmap getBitmapFromCache() {
        Log.e("chan", "===============getBitmapFromCache");
        return imageLoader.getBitmap("bitmap");
    }

    private void getBitmapFromInternet() {
        Log.e("chan", "===============getBitmapFromInternet");
        OkHttpClient okHttpClient = new OkHttpClient();
        Request request = new Request.Builder()
                .url(Path)
                .build();
        Call call = okHttpClient.newCall(request);
        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {

            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                byte[] Picture_bt = response.body().bytes();
                Message message = handler.obtainMessage();
                message.obj = Picture_bt;
                message.what = SUCCESS;
                handler.sendMessage(message);
            }
        });
    }

}
複製程式碼

這個方法很簡單,就是使用 okhttp 從網路載入一張圖片,如果不過在網路載入前就會先檢視快取裡面是否有這張圖片,如果存在就直接從快取載入。

3. LruCache 原理

LruCache 其實使用了 LinkedHashMap 雙向連結串列結構,現在分析下 LinkedHashMap 使用方法。

構造方法:

public LinkedHashMap(int initialCapacity,
    loat loadFactor,
    boolean accessOrder) {
    super(initialCapacity, loadFactor);
    this.accessOrder = accessOrder;
}
複製程式碼

當 accessOrder 為 true 時,這個集合的元素順序就會是訪問順序,也就是訪問了之後就會將這個元素放到集合的最後面。

LinkedHashMap < Integer, Integer > map = new LinkedHashMap < > (0, 0.75f, true);
map.put(0, 0);
map.put(1, 1);
map.put(2, 2);
map.put(3, 3);
map.get(1);
map.get(2);

for (Map.Entry < Integer, Integer > entry: map.entrySet()) {
    System.out.println(entry.getKey() + ":" + entry.getValue());

}
複製程式碼

列印結果:

0:0
3:3
1:1
2:2
複製程式碼

以下分別來分析 LruCache 的 put 和 get 方法。

3.1 put 方法分析:

現在以註釋的方式來解釋該方法的原理。

public final V put(K key, V value) {
    // 如果 key 或者 value 為 null,則丟擲異常
    if (key == null || value == null) {
        throw new NullPointerException("key == null || value == null");
    }

    V previous;
    synchronized(this) {
        // 加入元素的數量,在 putCount() 用到
        putCount++;

        // 回撥用 sizeOf(K key, V value) 方法,這個方法使用者自己實現,預設返回 1
        size += safeSizeOf(key, value);

        // 返回之前關聯過這個 key 的值,如果沒有關聯過則返回 null
        previous = map.put(key, value);

        if (previous != null) {
            // safeSizeOf() 預設返回 1
            size -= safeSizeOf(key, previous);
        }
    }

    if (previous != null) {
        // 該方法預設方法體為空
        entryRemoved(false, key, previous, value);
    }

    trimToSize(maxSize);

    return previous;
}

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!");
            }

            // 直到快取大小 size 小於或等於最大快取大小 maxSize,則停止迴圈
            if (size <= maxSize) {
                break;
            }

            // 取出 map 中第一個元素
            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);
    }
}

public Map.Entry<K, V> eldest() {
    return head;
}
複製程式碼

put() 方法其實重點就在於 trimToSize() 方法裡面,這個方法的作用就是判斷加入元素後是否超過最大快取數,如果超過就清除掉最少使用的元素。

3.2 get 方法分析

public final V get(K key) {
    if (key == null) {
        throw new NullPointerException("key == null");
    }

    V mapValue;
    synchronized(this) {
        // 獲取 Value
        mapValue = map.get(key);
        if (mapValue != null) {
            hitCount++;
            return mapValue;
        }
        missCount++;
    }

    ......
}

// LinkedHashMap get 方法
public V get(Object key) {
    Node < K, V > e;
    if ((e = getNode(hash(key), key)) == null) return null;
    if (accessOrder) afterNodeAccess(e);
    return e.value;
}

static class Node < K, V > implements Map.Entry < K, V > {
    final int hash;
    final K key;
    V value;
    Node < K, V > next;

    Node(int hash, K key, V value, Node < K, V > next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }

    public final K getKey() {
        return key;
    }
    public final V getValue() {
        return value;
    }
    public final String toString() {
        return key + "=" + value;
    }

    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    public final boolean equals(Object o) {
        if (o == this) return true;
        if (o instanceof Map.Entry) {
            Map.Entry <? , ?> e = (Map.Entry <? , ?> ) o;
            if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true;
        }
        return false;
    }
}

static class Entry<K,V> extends HashMap.Node<K,V> {
    Entry<K,V> before, after;
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}
複製程式碼

get() 方法其實最關鍵就是 afterNodeAccess(),現在重點分析:

3.2.1 關鍵方法 afterNodeAccess()

// 這個方法的作用就是將剛訪問過的元素放到集合的最後一位
void afterNodeAccess(Node < K, V > e) { 
    LinkedHashMap.Entry < K, V > last;
    if (accessOrder && (last = tail) != e) {
        // 將 e 轉換成 LinkedHashMap.Entry
        // b 就是這個節點之前的節點
        // a 就是這個節點之後的節點
        LinkedHashMap.Entry < K, V > p = (LinkedHashMap.Entry < K, V > ) e, b = p.before, a = p.after;

        // 將這個節點之後的節點置為 null
        p.after = null;

        // b 為 null,則代表這個節點是第一個節點,將它後面的節點置為第一個節點
        if (b == null) head = a;
        // 如果不是,則將 a 上前移動一位
        else b.after = a;
        // 如果 a 不為 null,則將 a 節點的元素變為 b
        if (a != null) a.before = b;
        else last = b;
        if (last == null) head = p;
        else {
            p.before = last;
            last.after = p;
        }
        tail = p;
        ++modCount;
    }
}
複製程式碼

連結串列情況可能性圖示:

LruCache 使用及原理

以下來分析情況一。

3.1.2.1 情況一:

LruCache 使用及原理

1. p.after = null;

圖示:

LruCache 使用及原理

2. b.after = a; a.before = b;

圖示:

LruCache 使用及原理

3. p.before = last; last.after = p;

圖示:

LruCache 使用及原理

情況一其實與其他情況基本一樣,這裡就不再贅述了。

4. 總結

  • LruCache 其實使用了 LinkedHashMap 維護了強引用物件
  • 總快取的大小一般是可用記憶體的 1/8,當超過總快取大小會刪除最少使用的元素,也就是內部 LinkedHashMap 的頭部元素
  • 當使用 get() 訪問元素後,會將該元素移動到 LinkedHashMap 的尾部

相關文章