目錄
1. 簡介
下面,將詳細介紹 LrhCache
演算法
2. LruCache演算法
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()
獲取快取物件時,就代表訪問了1次該元素 - 訪問後將會更新佇列,使得整個佇列是按照 訪問順序 排列
- 當呼叫
- 示意圖如下
上述更新過程是在 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
演算法、原理等,下面是部分總結
- 原理
- 示意圖
- 原始碼流程