Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 詳解基於LRU緩存的LruCache類及其在Android內存優化中的應用

詳解基於LRU緩存的LruCache類及其在Android內存優化中的應用

編輯:關於Android編程

LruCache

今天與大家分享一下圖片的緩存技術,利用它可以提高UI的流暢性、響應速度,給用戶好的體驗。

如何在內存中做緩存?

通過內存緩存可以快速加載緩存圖片,但會消耗應用的內存空間。LruCache類(通過兼容包可以支持到sdk4)很適合做圖片緩存,它通過LinkedHashMap保持圖片的強引用方式存儲圖片,當緩存空間超過設置定的限值時會釋放掉早期的緩存。

注:在過去,常用的內存緩存實現是通過SoftReference或WeakReference,但不建議這樣做。從Android2.3(API等級9)垃圾收集器開始更積極收集軟/弱引用,這使得它們相當無效。此外,在Android 3.0(API等級11)之前,存儲在native內存中的可見的bitmap不會被釋放,可能會導致應用程序暫時地超過其內存限制並崩潰。

什麼是LruCache?
LruCache實現原理是什麼?

要回答這個兩個問題,先要知道什麼是LRU。
LRU是Least Recently Used 的縮寫,翻譯過來就是“最近最少使用”,LRU緩存就是使用這種原理實現,簡單的說就是緩存一定量的數據,當超過設定的阈值時就把一些過期的數據刪除掉,比如我們緩存100M的數據,當總數據小於100M時可以隨意添加,當超過100M時就需要把新的數據添加進來,同時要把過期數據刪除,以確保我們最大緩存100M,那怎麼確定刪除哪條過期數據呢,采用LRU算法實現的話就是將最老的數據刪掉。利用LRU緩存,我們能夠提高系統的performance.

LruCache源碼分析

LruCache.java是 android.support.v4包引入的一個類,其實現原理就是基於LRU緩存算法,
要想實現LRU緩存,我們首先要用到一個類 LinkedHashMap。 用這個類有兩大好處:一是它本身已經實現了按照訪問順序的存儲,也就是說,最近讀取的會放在最前面,最不常讀取的會放在最後(當然,它也可以實現按照插入順序存儲)。第二,LinkedHashMap本身有一個方法用於判斷是否需要移除最不常讀取的數,但是,原始方法默認不需要移除(這是,LinkedHashMap相當於一個linkedlist),所以,我們需要override這樣一個方法,使得當緩存裡存放的數據個數超過規定個數後,就把最不常用的移除掉。LinkedHashMap的API寫得很清楚,推薦大家可以先讀一下。
要基於LinkedHashMap來實現LRU緩存,可以選擇inheritance, 也可以選擇 delegation, android源碼選擇的是delegation,而且寫得很漂亮。下面,就來剖析一下源碼的實現方法:

public class LruCache {
    //緩存 map 集合,要用LinkedHashMap    
    private final LinkedHashMap map;

    private int size; //已經存儲的大小
    private int maxSize; //規定的最大存儲空間

    private int putCount;  //put的次數
    private int createCount;  //create的次數
    private int evictionCount;  //回收的次數
    private int hitCount;  //命中的次數
    private int missCount;  //丟失的次數

    //實例化 Lru,需要傳入緩存的最大值,這個最大值可以是個數,比如對象的個數,也可以是內存的大小
    //比如,最大內存只能緩存5兆
    public LruCache(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        this.maxSize = maxSize;
        this.map = new LinkedHashMap(0, 0.75f, true);
    }

   //重置最大存儲空間
    public void resize(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }

        synchronized (this) {
            this.maxSize = maxSize;
        }
        trimToSize(maxSize);
    }

    //通過key返回相應的item,或者創建返回相應的item。相應的item會移動到隊列的頭部,
    // 如果item的value沒有被cache或者不能被創建,則返回null。
    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++;
        }

        //如果丟失了就試圖創建一個item
        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;
        }
    }

    //創建cache項,並將創建的項放到隊列的頭部
    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) {  //返回的先前的value值
                size -= safeSizeOf(key, previous);
            }
        }

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

        trimToSize(maxSize);
        return previous;
    }

    //清空cache空間
    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 || map.isEmpty()) {
                    break;
                }

                Map.Entry toEvict = map.entrySet().iterator().next();
                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key);
                size -= safeSizeOf(key, value);
                evictionCount++;
            }

            entryRemoved(true, key, value, null);
        }
    }

    //刪除key相應的cache項,返回相應的value
    public final V remove(K key) {
        if (key == null) {
            throw new NullPointerException("key == null");
        }

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

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

        return previous;
    }

    /**
     * Called for entries that have been evicted or removed. This method is
     * invoked when a value is evicted to make space, removed by a call to
     * {@link #remove}, or replaced by a call to {@link #put}. The default
     * implementation does nothing.
     * 當item被回收或者刪掉時調用。改方法當value被回收釋放存儲空間時被remove調用,
     * 或者替換item值時put調用,默認實現什麼都沒做
     * 

The method is called without synchronization: other threads may * access the cache while this method is executing. * * @param evicted true if the entry is being removed to make space, false * if the removal was caused by a {@link #put} or {@link #remove}. * true---為釋放空間被刪除;false---put或remove導致 * @param newValue the new value for {@code key}, if it exists. If non-null, * this removal was caused by a {@link #put}. Otherwise it was caused by * an eviction or a {@link #remove}. */ protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {} //當某Item丟失時會調用到,返回計算的相應的value或者null protected V create(K key) { return null; } private int safeSizeOf(K key, V value) { int result = sizeOf(key, value); if (result < 0) { throw new IllegalStateException("Negative size: " + key + "=" + value); } return result; } //這個方法要特別注意,跟我們實例化LruCache的maxSize要呼應,怎麼做到呼應呢,比如maxSize的大小為緩存 //的個數,這裡就是return 1就ok,如果是內存的大小,如果5M,這個就不能是個數了,就需要覆蓋這個方法,返回每個緩存 //value的size大小,如果是Bitmap,這應該是bitmap.getByteCount(); protected int sizeOf(K key, V value) { return 1; } //清空cacke public final void evictAll() { trimToSize(-1); // -1 will evict 0-sized elements } /** * For caches that do not override {@link #sizeOf}, this returns the number * of entries in the cache. For all other caches, this returns the sum of * the sizes of the entries in this cache. */ public synchronized final int size() { return size; } public synchronized final int maxSize() { return maxSize; } /** * Returns the number of times {@link #get} returned a value that was * already present in the cache. */ public synchronized final int hitCount() { return hitCount; } /** * Returns the number of times {@link #get} returned null or required a new * value to be created. */ public synchronized final int missCount() { return missCount; } public synchronized final int createCount() { return createCount; } public synchronized final int putCount() { return putCount; } //返回被回收的數量 public synchronized final int evictionCount() { return evictionCount; } //返回當前cache的副本,從最近最少訪問到最多訪問 public synchronized final Map snapshot() { return new LinkedHashMap(map); } @Override public synchronized final String toString() { int accesses = hitCount + missCount; int hitPercent = accesses != 0 ? (100 * hitCount / accesses) : 0; return String.format("LruCache[maxSize=%d,hits=%d,misses=%d,hitRate=%d%%]", maxSize, hitCount, missCount, hitPercent); } }

從源代碼中,我們可以清晰的看出LruCache的緩存機制。
此外,還有一個開源的使用磁盤緩存的方法DiskLruCache,源代碼:https://github.com/JakeWharton/DiskLruCache
其詳細的使用方法可以參考鏈接:http://www.tuicool.com/articles/JB7RNj

LruCache應用實例

根據上面的代碼,當我們用LruCache來緩存圖片時,一定要重寫protected int sizeOf(K key, V value) {}方法,否則,最大緩存的是數量而不是占用內存大小。重寫protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}方法,在裡面處理內存回收。
下面例子繼承LruCache,實現相關方法:

public class BitmapCache extends LruCache{
    private BitmapRemovedCallBack mEnterRemovedCallBack;

    public BitmapCache(int maxSize, BitmapRemovedCallBack callBack) {
        super(maxSize);
        mEnterRemovedCallBack = callBack;
    }

    //當緩存大於我們設定的最大值時,會調用這個方法,我們在這裡面做內存釋放操作
    @Override
    protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {
        super.entryRemoved(evicted, key, oldValue, newValue);
        if (evicted && oldValue != null){
        //在回收bitmap之前,務必要先在下面的回調方法中將bitmap設給的View的bitmapDrawable設為null
        //否則,bitmap被回收後,很容易出現cannot draw recycled bitmap的報錯。切記!
            mEnterRemovedCallBack.onBitmapRemoved(key);
            oldValue.recycle();
        }
    }

    //獲取每個 value 的大小
    @Override
    protected int sizeOf(K key, V value) {
        int size = 0;
        if (value != null) {
            size = value.getByteCount();
        }
        return size;
    }

    public interface BitmapRemovedCallBack{
        void onBitmapRemoved(K key);
    }
}

使用BitmapCache時,在構造方法中傳入最大緩存量和一個回掉方法就行:

private BitmapCache mMemoryCache;  

private BitmapCache.BitmapRemovedCallBack mEnteryRemovedCallBack =
            new BitmapCache.BitmapRemovedCallBack() {
        @Override
        public void onBitmapRemoved(String key) {
        //處理回收bitmap前,清空相關view的bitmap操作         
        }
    };
Override
protected void onCreate(Bundle savedInstanceState) { 
    // 獲取到可用內存的最大值,使用內存超出這個值會引起OutOfMemory異常。 
    // BitmapCache通過構造函數傳入緩存值,以bit為單位。 
    int memClass = ((ActivityManager) activity.getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass();
    // 使用單個應用最大可用內存值的1/8作為緩存的大小。 
    int cacheSize = 1024 * 1024 * memClass / 8;
    mMemoryCache = new BitmapCache(cacheSize, mEnteryRemovedCallBack);
} 

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

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

當向 ImageView 中加載一張圖片時,首先會在 BitmapCache 的緩存中進行檢查。如果找到了相應的鍵值,則會立刻更新ImageView ,否則開啟一個後台線程來加載這張圖片:

public void loadBitmap(int resId, ImageView imageView) { 
    final String imageKey = String.valueOf(resId); 
    final Bitmap bitmap = getBitmapFromMemCache(imageKey); 
    if (bitmap != null) { 
        imageView.setImageBitmap(bitmap); 
    } else { 
        imageView.setImageResource(R.drawable.image_placeholder); 
        BitmapLoadingTask task = new BitmapLoadingTask(imageView); 
        task.execute(resId); 
    } 
}

BitmapLoadingTask後台線程,把新加載出來的圖片以鍵值對形式放到緩存中:

class BitmapLoadingTask extends AsyncTask { 
    // 在後台加載圖片。 
    @Override
    protected Bitmap doInBackground(Integer... params) { 
        final Bitmap bitmap = decodeSampledBitmapFromResource( 
                getResources(), params[0], 100, 100); 
        addBitmapToMemoryCache(String.valueOf(params[0]), bitmap); 
        return bitmap; 
    } 
}

總結

1、LruCache 是基於 Lru算法實現的一種緩存機制;
2、Lru算法的原理是把近期最少使用的數據給移除掉,當然前提是當前數據的量大於設定的最大值。
3、LruCache 沒有真正的釋放內存,只是從 Map中移除掉數據,真正釋放內存還是要用戶手動釋放。
4、手動釋放bitmap的內存時,需要先清除相關view中的bitmap。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved