Android 記憶體快取:手把手教你學會LrhCache演算法
前言
Android LrhCache
1. 簡介
下面,將詳細介紹 LrhCache
演算法
2. LrhCache演算法
3. 實現原理
-
LrhCache
演算法的演算法核心 =LRU
演算法 +LinkedHashMap
資料結構 - 下面,我將先介紹
LRU
演算法 和LinkedHashMap
資料結構,最後再介紹LrhCache
演算法
3.1 LRU 演算法
- 定義:
Least Recently Used
,即 近期最少使用演算法 - 演算法原理:當快取滿時,優先淘汰 近期最少使用的快取物件
採用
LRU
演算法的快取型別:記憶體快取(LrhCache
) 、 硬碟快取(DisLruCache
)
3.2 LinkedHashMap 介紹
- 資料結構 = 陣列 +單鏈表 + 雙向連結串列
- 其中,雙向連結串列 實現了 儲存順序 = 訪問順序 / 插入順序
- 使得
LinkedHashMap
中的<key,value>
對 按照一定順序進行排列 - 通過 建構函式 指定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()
- 作用:獲取快取 & 更新佇列
get()
- 示意圖如下
上述更新過程是在 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
演算法、原理等,下面是部分總結
- 原理
- 示意圖
- 原始碼流程