轉:LruCache算法

目錄

內存緩存

1. 簡介

示意圖

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

2. LruCache算法

示意圖

3. 實現原理

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

3.1 LRU 算法

  • 定義:Least Recently Used,即 近期最少使用算法
  • 算法原理:當緩存滿時,優先淘汰 近期最少使用的緩存對象

    採用 LRU 算法的緩存類型:內存緩存(LrhCache) 、 硬盤緩存(DisLruCacheandroid

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()bash

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()中完成app

  • 源碼分析
/** 
  * 使用函數(以加載圖片爲例)
  **/ 
  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()的源碼分析完畢。ide

7. 總結

本文全面講解了內存緩存的相關知識,含LrhCache算法、原理等,下面是部分總結函數

  • 原理
    示意圖
  • 示意圖
    示意圖
  • 源碼流程
    示意圖
相關文章
相關標籤/搜索