轉:LruCache演算法

chamwarren發表於2018-11-28

目錄

記憶體快取

1. 簡介

示意圖

下面,將詳細介紹 LrhCache演算法

2. LruCache演算法

示意圖

3. 實現原理

  • LrhCache演算法的演算法核心 = LRU 演算法 + LinkedHashMap資料結構
  • 下面,我將先介紹LRU 演算法 和 LinkedHashMap資料結構,最後再介紹LrhCache演算法

3.1 LRU 演算法

  • 定義:Least Recently Used,即 近期最少使用演算法
  • 演算法原理:當快取滿時,優先淘汰 近期最少使用的快取物件

    採用 LRU 演算法的快取型別:記憶體快取(LrhCache) 、 硬碟快取(DisLruCache

3.2 LinkedHashMap 介紹

  • 資料結構 = 陣列 +單連結串列 + 雙向連結串列
  • 其中,雙向連結串列 實現了 儲存順序 = 訪問順序 / 插入順序
    1. 使得LinkedHashMap 中的<key,value>對 按照一定順序進行排列
    2. 通過 建構函式 指定LinkedHashMap中雙向連結串列的結構是訪問順序 or 插入順序
/** 
  * LinkedHashMap 建構函式
  * 引數accessOrder = true時,儲存順序(遍歷順序) = 外部訪問順序;為false時,儲存順序(遍歷順序) = 插入順序
  **/ 
  public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {

        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;

    }複製程式碼
  • 例項演示

accessOrder 引數設定為true時,儲存順序(遍歷順序) = 外部訪問順序

  /** 
    * 例項演示
    **/ 
    // 1. accessOrder引數設定為true時
    LinkedHashMap<Integer, Integer> map = new LinkedHashMap<>(0, 0.75f, true);

    // 2. 插入資料
    map.put(0, 0);
    map.put(1, 1);
    map.put(2, 2);
    map.put(3, 3);
    map.put(4, 4);
    map.put(5, 5);
    map.put(6, 6);

    // 3. 訪問資料
        map.get(1);
        map.get(2);

     // 遍歷獲取LinkedHashMap內的資料
     for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
            System.out.println(entry.getKey() + ":" + entry.getValue());

        }

  /** 
    * 測試結果
    **/ 
   0:0
   3:3
   4:4
   5:5
   6:6
   1:1
   2:2

// 即實現了 最近訪問的物件 作為 最後輸出
// 該邏輯 = LrhCache快取演算法思想
// 可見LruCache的實現是利用了LinkedHashMap資料結構的實現原理
// 請看LruCache的構造方法

 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);
        // 建立LinkedHashMap時傳入true。即採用了儲存順序 = 外界訪問順序 = 最近訪問的物件 作為 最後輸出
    }複製程式碼

3.3 LrhCache 演算法原理

示意圖

  • 示意圖

示意圖

4. 使用流程

/** 
  * 使用流程(以載入圖片為例)
  **/ 

    private LruCache<String, Bitmap> mMemoryCache; 

    // 1. 獲得虛擬機器能提供的最大記憶體
    // 注:超過該大小會丟擲OutOfMemory的異常 
    final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); 

    // 2. 設定LruCache快取的大小 = 一般為當前程式可用容量的1/8
    // 注:單位 = Kb
    // 設定準則
    //  a. 還剩餘多少記憶體給你的activity或應用使用
    //  b. 螢幕上需要一次性顯示多少張圖片和多少圖片在等待顯示
    //  c. 手機的大小和密度是多少(密度越高的裝置需要越大的 快取)
    //  d. 圖片的尺寸(決定了所佔用的記憶體大小)
    //  e. 圖片的訪問頻率(頻率高的在記憶體中一直儲存)
    //  f. 儲存圖片的質量(不同畫素的在不同情況下顯示)
    final int cacheSize = maxMemory / 8; 

    // 3. 重寫sizeOf方法:計算快取物件的大小(此處的快取物件 = 圖片)
    mMemoryCache = new LruCache<String, Bitmap>(cacheSize) { 

        @Override 
        protected int sizeOf(String key, Bitmap bitmap) { 

            return bitmap.getByteCount() / 1024; 
            // 此處返回的是快取物件的快取大小(單位 = Kb) ,而不是item的個數
            // 注:快取的總容量和每個快取物件的大小所用單位要一致
            // 此處除1024是為了讓快取物件的大小單位 = Kb

        } 
    }; 

    // 4. 將需快取的圖片 加入到快取
    mMemoryCache.put(key, bitmap); 

    // 5. 當 ImageView 載入圖片時,會先在LruCache中看有沒有快取該圖片:若有,則直接獲取
    mMemoryCache.get(key); 複製程式碼

5. 例項講解

  • 本例項以快取圖片為例項講解
  • 具體程式碼

請看註釋
MainActivity.java

public class MainActivity extends AppCompatActivity {

    public static final String TAG = "carsonTest:";
    private LruCache<String, Bitmap> mMemoryCache;
    private ImageView mImageView;
    private Button button;

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

        // 1. 獲得虛擬機器能提供的最大記憶體
        // 注:超過該大小會丟擲OutOfMemory的異常
        final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

        // 2. 設定LruCache快取的大小 = 一般為當前程式可用容量的1/8
        // 注:單位 = Kb
        final int cacheSize = maxMemory / 8;

        // 3. 重寫sizeOf方法:計算快取物件的大小(此處的快取物件 = 圖片)
        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {

            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                return bitmap.getByteCount() / 1024;
                // 此處返回的是快取物件的快取大小(單位 = Kb) ,而不是item的個數
                // 注:快取的總容量和每個快取物件的大小所用單位要一致
                // 此處除1024是為了讓快取物件的大小單位 = Kb

            }
        };

        // 4. 點選按鈕,則載入圖片
        mImageView = (ImageView)findViewById(R.id.image);
        button = (Button)findViewById(R.id.btn);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                // 載入圖片 ->>分析1
                loadBitmap("test",mImageView);
            }

        });

    }

    /**
     * 分析1:載入圖片
     * 載入前,先從記憶體快取中讀取;若無,則再從資料來源中讀取
     **/
    public void loadBitmap(String key, ImageView imageView) {

        // 讀取圖片前,先從記憶體快取中讀取:即看記憶體快取中是否快取了該圖片
        // 1. 若有快取,則直接從記憶體中載入
        Bitmap bitmap = mMemoryCache.get(key);

        if (bitmap != null) {
            mImageView.setImageBitmap(bitmap);
            Log.d(TAG, "從快取中載入圖片 ");

            // 2. 若無快取,則從資料來源載入(此處選擇在本地載入) & 新增到快取
        } else {
            Log.d(TAG, "從資料來源(本地)中載入: ");

            // 2.1 從資料來源載入
            mImageView.setImageResource(R.drawable.test1);

            // 2.1 新增到快取
            // 注:在新增到快取前,需先將資原始檔構造成1個BitMap物件(含設定大小)
            Resources res = getResources();
            Bitmap bm = BitmapFactory.decodeResource(res, R.drawable.test1);

            // 獲得圖片的寬高
            int width = bm.getWidth();
            int height = bm.getHeight();

            // 設定想要的大小
            int newWidth = 80;
            int newHeight = 80;

            // 計算縮放比例
            float scaleWidth = ((float) newWidth) / width;
            float scaleHeight = ((float) newHeight) / height;
            // 取得想要縮放的matrix引數
            Matrix matrix = new Matrix();
            matrix.postScale(scaleWidth, scaleHeight);
            // 構造成1個新的BitMap物件
            Bitmap bitmap_s = Bitmap.createBitmap(bm, 0, 0, width, height, matrix, true);

            // 新增到快取
            if (mMemoryCache.get(key) == null) {
                mMemoryCache.put(key, bitmap_s);
                Log.d(TAG, "新增到快取: " + (mMemoryCache.get(key)));
            }

        }
    }

}複製程式碼

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:focusableInTouchMode="true"
    android:orientation="vertical">

    <ImageView
       android:id="@+id/image"
       android:layout_width="200dp"
       android:layout_height="200dp"
       android:layout_gravity="center"

        />

    <Button
        android:id="@+id/btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:text="點選載入"
        android:layout_gravity="center"
        />

</LinearLayout>複製程式碼
  • 測試結果
    第1次點選載入圖片時,由於無快取則從本地載入
    第2次(以後)點選載入圖片時,由於有快取,所以直接從快取中讀取
    示意圖

6. 原始碼分析

此處主要分析 寫入快取 & 獲取快取 ,即put()get()

6.1 新增快取:put()

  • 原始碼分析
/** 
  * 使用函式(以載入圖片為例)
  **/ 
  mMemoryCache.put(key,bitmap);

/** 
  * 原始碼分析
  **/ 
 public final V put(K key, V value) {

        // 1. 判斷 key 與 value是否為空
        //  若二者之一味空,否則丟擲異常
        if (key == null || value == null) {
            throw new NullPointerException("key == null || value == null");
        }

        V previous;

        synchronized (this) {

            // 2. 插入的快取物件值加1
            putCount++; 

            // 3. 增加已有快取的大小
            size += safeSizeOf(key, value); 

            // 4. 向map中加入快取物件
            previous = map.put(key, value);

            // 5. 若已有快取物件,則快取大小恢復到之前
            if (previous != null) {
                size -= safeSizeOf(key, previous);
            }
        }

        // 6. 資源回收(移除舊快取時會被呼叫)
        // entryRemoved()是個空方法,可自行實現
        if (previous != null) {
            entryRemoved(false, key, previous, value);
        }

        // 7. 新增快取物件後,呼叫需判斷快取是否已滿
        // 若滿了就刪除近期最少使用的物件-->分析2
        trimToSize(maxSize);

        return previous;
    }

/** 
  * 分析1:trimToSize(maxSize)
  * 原理:不斷刪除LinkedHashMap中隊尾的元素,即近期最少訪問的元素,直到快取大小 < 最大值
  **/ 
  public void trimToSize(int maxSize) {

        //死迴圈
        while (true) {
            K key;
            V value;
            synchronized (this) {
                // 判斷1:若 map為空 & 快取size ≠ 0 或 快取size < 0,則丟擲異常
                if (size < 0 || (map.isEmpty() && size != 0)) {
                    throw new IllegalStateException(getClass().getName()
                            + ".sizeOf() is reporting inconsistent results!");
                }

                // 判斷2:若快取大小size < 最大快取 或 map為空,則不需刪除快取物件,跳出迴圈
                if (size <= maxSize || map.isEmpty()) {
                    break;
                }

                // 開始刪除快取物件
                // 使用迭代器獲取第1個物件,即隊尾的元素 = 近期最少訪問的元素
                Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
                key = toEvict.getKey();
                value = toEvict.getValue();
                // 刪除該物件 & 更新快取大小
                map.remove(key);
                size -= safeSizeOf(key, value);
                evictionCount++;
            }
            entryRemoved(true, key, value, null);
        }
    }
複製程式碼

至此,關於新增快取:put()的原始碼分析完畢。

6.2 獲取快取:get()

  • 作用:獲取快取 & 更新佇列
    1. 當呼叫 get() 獲取快取物件時,就代表訪問了1次該元素
    2. 訪問後將會更新佇列,使得整個佇列是按照 訪問順序 排列
  • 示意圖如下
    示意圖

上述更新過程是在 get()中完成

  • 原始碼分析
/** 
  * 使用函式(以載入圖片為例)
  **/ 
  mMemoryCache.get(key);

/** 
  * 原始碼分析
  **/ 
  public final V get(K key) {

        // 1. 判斷輸入的合法性:若key為空,則丟擲異常
        if (key == null) {
            throw new NullPointerException("key == null");
        }

        V mapValue;
        synchronized (this) {

            // 2. 獲取對應的快取物件 & 將訪問的元素 更新到 佇列頭部->>分析3
            mapValue = map.get(key);

            if (mapValue != null) {
                hitCount++;
                return mapValue;
            }
            missCount++;
        }

/** 
  * 分析1:map.get(key)
  * 實際上是 LinkedHashMap.get() 
  **/ 
  public V get(Object key) {

        // 1. 獲取對應的快取物件
        LinkedHashMapEntry<K,V> e = (LinkedHashMapEntry<K,V>)getEntry(key);
        if (e == null)
            return null;

        // 2. 將訪問的元素更新到佇列頭部 ->>分析4
        e.recordAccess(this);

        return e.value;
    }
/** 
  * 分析2:recordAccess()
  **/ 
  void recordAccess(HashMap<K,V> m) {

            LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;

            // 1. 判斷LinkedHashMap儲存順序是否按訪問排序排序:根據建構函式傳入的引數accessOrder判斷
            if (lm.accessOrder) {
                lm.modCount++;
                // 2. 刪除此元素
                remove();
                // 3. 將此元素移動到佇列的頭部
                addBefore(lm.header);
            }
        }
複製程式碼

至此,關於獲取快取:get()的原始碼分析完畢。

7. 總結

本文全面講解了記憶體快取的相關知識,含LrhCache演算法、原理等,下面是部分總結

  • 原理
    示意圖
  • 示意圖
    示意圖
  • 原始碼流程
    示意圖


相關文章