下面,將詳細介紹 LrhCache
算法java
LrhCache
算法的算法核心 = LRU
算法 + LinkedHashMap
數據結構LRU
算法 和 LinkedHashMap
數據結構,最後再介紹LrhCache
算法Least Recently Used
,即 近期最少使用算法採用
LRU
算法的緩存類型:內存緩存(LrhCache
) 、 硬盤緩存(DisLruCache
)android
- 使得
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。即採用了存儲順序 = 外界訪問順序 = 最近訪問的對象 做爲 最後輸出
}複製代碼
/**
* 使用流程(以加載圖片爲例)
**/
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); 複製代碼
請看註釋
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>複製代碼
此處主要分析 寫入緩存 & 獲取緩存 ,即put()
、 get()
bash
/**
* 使用函數(以加載圖片爲例)
**/
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()
的源碼分析完畢。數據結構
- 當調用
get()
獲取緩存對象時,就表明訪問了1次該元素- 訪問後將會更新隊列,使得整個隊列是按照 訪問順序 排列
上述更新過程是在 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
本文全面講解了內存緩存的相關知識,含LrhCache
算法、原理等,下面是部分總結函數