天天看點

【安卓中的緩存政策系列】安卓緩存之記憶體緩存LruCache

緩存政策在移動端裝置上是非常重要的,尤其是在圖檔加載這個場景下,因為圖檔相對而言比較大會花費使用者較多的流量,是以可用緩存方式來解決,即當程式第一次從網絡上擷取圖檔的時候,就将其緩存到儲存設備上,這樣在使用者下次使用這張圖檔時就不用從網絡上再次擷取,這樣就能為使用者節省一定的流量。這個功能目前絕大部分主流APP都會使用,如騰訊QQ,微信。但很多時候為了提高APP的使用者體驗,我們還需要把圖檔在記憶體中緩存一份,比如ListView,我們知道LIstView會在使用者将某些圖檔移出螢幕後将其進行回收,此時垃圾回收器會認為你不再持有這些圖檔的引用,進而對這些圖檔進行GC操作。可是為了能讓程式快速運作,在界面上迅速地加載圖檔,必須要考慮到某些圖檔被回收之後,使用者又将它重新滑入螢幕這種情況,而這種情況在ListView,GridView這種控件中出現是非常頻繁的。采用記憶體緩存技術可以很好的解決上述問題,記憶體緩存技術允許控件可以快速地重新加載那些處理過的圖檔。記憶體緩存技術主要是通過LruCache這個類來完成的,下面從源碼的角度詳細講解LruCache這個類,然後在此基礎上講解如何使用LruCache,讓讀者知其然更知其是以然。

一LruCache類:

首先我們來看一下類的定義及其構造函數:

public class LruCache<K, V>

 public LruCache(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        this.maxSize = maxSize;
        this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
    }
           

可以看到LruCache類使用泛型參數,其構造器參數為int型,該參數用來指定LruCache的大小,另外從其構造函數的實作過程來看,可以知道LruCache的底層是使用LinkedHashMap<K, V>來實作的,即LruCache使用一個強引用(strong referenced)的LinkedHashMap儲存最近引用的對象。(A cache that holds strong references to a limited number of values.)

然後我麼來看一下其重要的方法:

public final V put(K key, V value) {
        if (key == null || value == null) {
            throw new NullPointerException("key == null || value == null");
        }

        V previous;
        synchronized (this) {
            putCount++;
            size += safeSizeOf(key, value);
            previous = map.put(key, value);
            if (previous != null) {
                size -= safeSizeOf(key, previous);
            }
        }

        if (previous != null) {
            entryRemoved(false, key, previous, value);
        }

        trimToSize(maxSize);
        return previous;
    }



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

        V mapValue;
        synchronized (this) {
            mapValue = map.get(key);
            if (mapValue != null) {
                hitCount++;
                return mapValue;
            }
            missCount++;
        }
 /*
         * Attempt to create a value. This may take a long time, and the map
         * may be different when create() returns. If a conflicting value was
         * added to the map while create() was working, we leave that value in
         * the map and release the created value.
         */


        V createdValue = create(key);
        if (createdValue == null) {
            return null;
        }


        synchronized (this) {
            createCount++;
            mapValue = map.put(key, createdValue);


            if (mapValue != null) {
                // There was a conflict so undo that last put
                map.put(key, mapValue);
            } else {
                size += safeSizeOf(key, createdValue);
            }
        }


        if (mapValue != null) {
            entryRemoved(false, key, createdValue, mapValue);
            return mapValue;
        } else {
            trimToSize(maxSize);
            return createdValue;
        }
    }

 protected int sizeOf(K key, V value) {
        return 1;
    }
           

之是以把這三個方法拿出來講解,是因為這三個方法它對應着我們實作的LruCache記憶體緩存政策的建立緩存,添加key到緩存,從緩存中擷取V這三個最重要的功能,通常我們在建立緩存時會重寫上述的sizeOf(K key, V value),在該方法中傳回我們要建立的記憶體緩存的大小。而put與get則相對複雜些,我們首先來分析一下put方法。

可以看到put方法作用是緩存key,同時會将key移動到隊列頭部Caches {@code value} for {@code key}. The value is moved to the head of the queue.,另外它會傳回與的key對應的先前的V,(return the previous value mapped by {@code key})。采用的是同步方式來實作的。

接下來我們看一下get方法。

從get方法中可以看到get包括兩種情況:

1取的時候命中:直接傳回與key對應的V。

2取的時候未命中:根據key産生一個V,然後調用put方法将其放入。

當get方法會将傳回的值移動到隊列頭部。If a value was returned, it is moved to the  head of the queue。

從put與get方法可以看到,put與get時會将該元素放到隊列的頭部,因為無論是put還是get都表示該元素目前被使用過,是以會将其放到隊列的頭部,當緩存中的元素超過maxSize時,會通過trimToSize函數來去除緩存中最久的元素( Map.Entry<K, V> toEvict = map.eldest();),這就是所謂的LRU算法,即最近最少使用算法,即當當緩存中的元素超過maxSize時會淘汰最近最少使用的元素。下面是trimToSize的源碼:

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

下面是LinkedHashMap中eldest()函數的代碼:

public Entry<K, V> eldest() {
        LinkedEntry<K, V> eldest = header.nxt;
        return eldest != header ? eldest : null;
    }
           

通過上述代碼的分析相信看官對LruCache的思想已經非常熟悉了,LruCache的底層是通過LinkedHashMap來實作的,當建立一個LruCache的對象時會讓我們傳入一個int型的maxSize,當我們向LruCache中put與get元素時會将該元素放到緩存隊列的對頭,當put元素超過maxSize時(這也是為何要傳入maxSize參數的原因),會通過trimToSize函數來去除緩存中最久的元素,具體是通過Map.Entry<K, V> toEvict = map.eldest();來擷取最久的元素,然後通過remove(key)的方式将其移除。

二LruCache的使用:

這個類是3.1版本中提供的,如果要在更早的Android版本中使用,則需要導入android-support-v4的jar包。

我們以BitMap對象來建立LruCache緩存為例,首先正如我在前面講述的,一個LruCache緩存政策至少包含三個功能,即建立緩存,向緩存中添加元素,從緩存中擷取元素。

代碼如下:

private LruCache<String, Bitmap> mMemoryCache;

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    // Get max available VM memory, exceeding this amount will throw an
    // OutOfMemory exception. Stored in kilobytes as LruCache takes an
    // int in its constructor.
    final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

    // Use 1/8th of the available memory for this memory cache.
    final int cacheSize = maxMemory / 8;

    mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
        @Override
        protected int sizeOf(String key, Bitmap bitmap) {
            // The cache size will be measured in kilobytes rather than
            // number of items.
            return bitmap.getByteCount() / 1024;
        }
    };
    ...
}

public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
    if (getBitmapFromMemCache(key) == null) {
        mMemoryCache.put(key, bitmap);
    }
}

public Bitmap getBitmapFromMemCache(String key) {
    return mMemoryCache.get(key);
}
           

我們說過緩存的目的就是為了當加載某個圖檔時首先從LruCache 中檢查是否存在這個Bitmap。如果确實存在,它會立即被用來顯示到ImageView上,如果不存在,則會開啟一個背景線程去處理顯示該Bitmap任務。是以我們還需要為其添加一個loadBitmap的功能,代碼如下:

public void loadBitmap(int resId, ImageView imageView) {
    final String imageKey = String.valueOf(resId);

    final Bitmap bitmap = getBitmapFromMemCache(imageKey);
    if (bitmap != null) {
        mImageView.setImageBitmap(bitmap);
    } else {
        mImageView.setImageResource(R.drawable.image_placeholder);
        BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
        task.execute(resId);
    }
}
           

其中的BitmapWorkerTask 需要把解析好的Bitmap添加到記憶體緩存中:

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    ...
    // Decode image in background.
    @Override
    protected Bitmap doInBackground(Integer... params) {
        final Bitmap bitmap = decodeSampledBitmapFromResource(
                getResources(), params[0], 100, 100));
        addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
        return bitmap;
    }
    ...
}
           

好了,以上就是本人了解的關于LruCache的知識,看官如果覺得不錯,請點選下方的”頂“或“贊”按鈕給我一點小小的鼓勵哦!

【安卓中的緩存政策系列】安卓緩存之記憶體緩存LruCache

,另外看官還可以看看我的其它部落格的文章哦!

【安卓中的緩存政策系列】安卓緩存之記憶體緩存LruCache