前言
最近在刷面試題垫桂,遇到一個(gè)問題师幕,關(guān)于緩存的原理的,所以在這里幾個(gè)筆記诬滩,關(guān)于緩存很多大牛都說過了霹粥,我只是做個(gè)筆記,下面的很多都是網(wǎng)上查看到的碱呼,并非原創(chuàng)
目錄
-
一:Android 緩存策略
- 內(nèi)存緩存(LruCache)
- 2.磁盤緩存(文件緩存)——DiskLruCache分析
- 3 ASimpleCache
-
二:使用
- LRU使用
- DiskLruCache
-
三 源碼分析
- LRU 源碼分析
一:Android 緩存策略
1. 內(nèi)存緩存(LruCache)
LRU蒙挑,全稱Least Rencetly Used宗侦,即最近最少使用愚臀,是一種非常常用的置換算法,也即淘汰最長時(shí)間未使用的對象矾利。LRU在操作系統(tǒng)中的頁面置換算法中廣泛使用姑裂,我們的內(nèi)存或緩存空間是有限的,當(dāng)新加入一個(gè)對象時(shí)男旗,造成我們的緩存空間不足了舶斧,此時(shí)就需要根據(jù)某種算法對緩存中原有數(shù)據(jù)進(jìn)行淘汰貨刪除,而LRU選擇的是將最長時(shí)間未使用的對象進(jìn)行淘汰察皇。
2. 磁盤緩存(文件緩存)——DiskLruCache分析
JakeWharton/DiskLruCache
不同于LruCache茴厉,LruCache是將數(shù)據(jù)緩存到內(nèi)存中去泽台,而DiskLruCache是外部緩存,例如可以將網(wǎng)絡(luò)下載的圖片永久的緩存到手機(jī)外部存儲中去矾缓,并可以將緩存數(shù)據(jù)取出來使用怀酷,DiskLruCache不是google官方所寫,但是得到了官方推薦
3. ASimpleCache
二:使用
1. LRU使用
(1) 實(shí)例化
看下源碼
public class LruCache<K, V> {}
鍵值對的形式
下面是往上很多標(biāo)準(zhǔn)的寫法嗜闻,用于保存圖片
private void init_Lru() {
//設(shè)置LruCache緩存的大小蜕依,一般為當(dāng)前進(jìn)程可用容量的1/8。
int maxMemory = (int) (Runtime.getRuntime().totalMemory() / 1024);
int cacheSize = maxMemory / 8;
// 重寫sizeOf方法琉雳,計(jì)算出要緩存的每張圖片的大小样眠。
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
// 重寫sizeOf方法,計(jì)算出要緩存的每張圖片的大小翠肘。
return value.getRowBytes() * value.getHeight() / 1024;
}
};
}
(2)保存
public final V put(K key, V value) {}
(3)獲取
public final V get(K key) {}
2. DiskLruCache
(1) 權(quán)限
因?yàn)橐僮魍獠看鎯﹂苁员仨氁燃由蠙?quán)限:
<!-- 在SDCard中創(chuàng)建與刪除文件權(quán)限 -->
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
<!-- 往SDCard寫入數(shù)據(jù)權(quán)限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
另外要從網(wǎng)絡(luò)下載圖片,還要加上權(quán)限:
<uses-permission android:name="android.permission.INTERNET" />
(2)初始化DiskLruCache
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize){}
參數(shù)說明
File directory 數(shù)據(jù)的緩存地址
int appVersion 當(dāng)前應(yīng)用程序的版本號
- int valueCount 指定同一個(gè)key可以對應(yīng)多少個(gè)緩存文件锯茄,基本都是傳1
- long maxSize 定最多可以緩存多少字節(jié)的數(shù)據(jù)
獲取緩存地址的方法
/***
*
* @param context
* @param uniqueName 緩存地址的名字
* @return 緩存地址
*/
public File getDiskCacheDir(Context context, String uniqueName) {
String cachePath;
//當(dāng)SD卡存在或者SD卡不可被移除
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
|| !Environment.isExternalStorageRemovable()) {
// 路徑/sdcard/Android/data/<application package>/cache/uniqueName
cachePath = context.getExternalCacheDir().getPath();
} else {
// 路徑/data/data/<application package>/cache/uniqueName
cachePath = context.getCacheDir().getPath();
}
return new File(cachePath + File.separator + uniqueName);
}
獲取App版本
/***
*
* @param context
* @return App 版本
*/
public int getAppVersion(Context context) {
try {
PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
return info.versionCode;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return 1;
}
初始化
File test = getDiskCacheDir(this, "Bitmap");
int appVersion = getAppVersion(this);
long maxSize = 10 * 1024 * 1024;
try {
mDiskLruCache = DiskLruCache.open(test, appVersion, 1, maxSize);
} catch (IOException e) {
e.printStackTrace();
}
(3) 存數(shù)據(jù)
String key = hashKeyForDisk(url);
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
OuputStream os = editor.newOutputStream(0);
//進(jìn)行提交才能使寫入生效
editor.commit();
//表示放棄此次寫入
editor.abort();
生成MD5
public String hashKeyForDisk(String key) {
String cacheKey;
try {
final MessageDigest mDigest = MessageDigest.getInstance("MD5");
mDigest.update(key.getBytes());
cacheKey = bytesToHexString(mDigest.digest());
} catch (NoSuchAlgorithmException e) {
cacheKey = String.valueOf(key.hashCode());
}
return cacheKey;
}
private String bytesToHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(0xFF & bytes[i]);
if (hex.length() == 1) {
sb.append('0');
}
sb.append(hex);
}
return sb.toString();
}
(4)取數(shù)據(jù)
DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
if (snapShot != null) {
InputStream is = snapShot.getInputStream(0);
}
(5)刪除數(shù)據(jù)
public synchronized boolean remove(String key) throws IOException
(6)其他API
size()
這個(gè)方法會返回當(dāng)前緩存路徑下所有緩存數(shù)據(jù)的總字節(jié)數(shù)厢塘,以byte為單位,如果應(yīng)用程序中需要在界面上顯示當(dāng)前緩存數(shù)據(jù)的總大小肌幽,就可以通過調(diào)用這個(gè)方法計(jì)算出來flush()
這個(gè)方法用于將內(nèi)存中的操作記錄同步到日志文件(也就是journal文件)當(dāng)中晚碾。這個(gè)方法非常重要,因?yàn)镈iskLruCache能夠正常工作的前提就是要依賴于journal文件中的內(nèi)容喂急。前面在講解寫入緩存操作的時(shí)候我有調(diào)用過一次這個(gè)方法格嘁,但其實(shí)并不是每次寫入緩存都要調(diào)用一次flush()方法的,頻繁地調(diào)用并不會帶來任何好處廊移,只會額外增加同步j(luò)ournal文件的時(shí)間糕簿。比較標(biāo)準(zhǔn)的做法就是在Activity的onPause()方法中去調(diào)用一次flush()方法就可以了
close()
這個(gè)方法用于將DiskLruCache關(guān)閉掉,是和open()方法對應(yīng)的一個(gè)方法狡孔。關(guān)閉掉了之后就不能再調(diào)用DiskLruCache中任何操作緩存數(shù)據(jù)的方法懂诗,通常只應(yīng)該在Activity的onDestroy()方法中去調(diào)用close()方法。delete()
這個(gè)方法用于將所有的緩存數(shù)據(jù)全部刪除苗膝,比如說網(wǎng)易新聞中的那個(gè)手動(dòng)清理緩存功能殃恒,其實(shí)只需要調(diào)用一下DiskLruCache的delete()方法就可以實(shí)現(xiàn)了。
三. 源碼分析
1. LRU 源碼分析
屬性
public class LruCache<K, V> {
private int size;// 當(dāng)前大小
private int maxSize;// 最大容量
private int putCount;// put次數(shù)
private int createCount;// 創(chuàng)建次數(shù)
private int evictionCount;// 回收次數(shù)
private int hitCount;// 命中次數(shù)
private int missCount;// 未命中次數(shù)
}
構(gòu)造方法
/**
* @param maxSize for caches that do not override {@link #sizeOf}, this is
* the maximum number of entries in the cache. For all other caches,
* this is the maximum sum of the sizes of the entries in this cache.
*/
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);
}
- 設(shè)置了最大容量
- 可以看到有一個(gè)LinkedHashMap辱揭,這個(gè)是核心离唐,用于儲存緩存的對象,
- 在LinkedHashMap中的第三個(gè)參數(shù)指定為true问窃,表示這個(gè)LinkedHashMap將是基于數(shù)據(jù)的訪問順序進(jìn)行排序亥鬓。為false,則為插入順序(這里就是核心)
sizeOf && safeSizeOf
private int safeSizeOf(K key, V value) {
int result = sizeOf(key, value);
if (result < 0) {
throw new IllegalStateException("Negative size: " + key + "=" + value);
}
return result;
}
/**
* Returns the size of the entry for {@code key} and {@code value} in
* user-defined units. The default implementation returns 1 so that size
* is the number of entries and max size is the maximum number of entries.
*
* <p>An entry's size must not change while it is in the cache.
*/
protected int sizeOf(K key, V value) {
return 1;
}
- 由于各種數(shù)據(jù)類型大小測量的標(biāo)準(zhǔn)不統(tǒng)一域庇,具體測量的方法應(yīng)該由使用者來實(shí)現(xiàn)
保存數(shù)據(jù)
下面代碼來自 http://blog.csdn.net/shakespeare001/article/details/51695358
/**
* 給對應(yīng)key緩存value嵌戈,并且將該value移動(dòng)到鏈表的尾部覆积。
*/
public final V put(K key, V value) {
if (key == null || value == null) {
throw new NullPointerException("key == null || value == null");
}
V previous;
synchronized (this) {
// 記錄 put 的次數(shù)
putCount++;
// 通過鍵值對,計(jì)算出要保存對象value的大小熟呛,并更新當(dāng)前緩存大小
size += safeSizeOf(key, value);
/*
* 如果 之前存在key技健,用新的value覆蓋原來的數(shù)據(jù), 并返回 之前key 的value
* 記錄在 previous
*/
previous = map.put(key, value);
// 如果之前存在key惰拱,并且之前的value不為null
if (previous != null) {
// 計(jì)算出 之前value的大小雌贱,因?yàn)榍懊鎠ize已經(jīng)加上了新的value數(shù)據(jù)的大小,此時(shí)偿短,需要再次更新size欣孤,減去原來value的大小
size -= safeSizeOf(key, previous);
}
}
// 如果之前存在key,并且之前的value不為null
if (previous != null) {
/*
* previous值被剔除了昔逗,此次添加的 value 已經(jīng)作為key的 新值
* 告訴 自定義 的 entryRemoved 方法
*/
entryRemoved(false, key, previous, value);
}
//裁剪緩存容量(在當(dāng)前緩存數(shù)據(jù)大小超過了總?cè)萘縨axSize時(shí)降传,才會真正去執(zhí)行LRU)
trimToSize(maxSize);
return previous;
}
- key和value判空,說明LruCache中不允許key和value為null勾怒;
- 通過safeSizeOf()獲取要加入對象數(shù)據(jù)的大小婆排,并更新當(dāng)前緩存數(shù)據(jù)的大小笔链;
- 將新的對象數(shù)據(jù)放入到緩存中段只,即調(diào)用LinkedHashMap的put方法,如果原來存在該key時(shí)鉴扫,直接替換掉原來的value值赞枕,并返回之前的value值,得到之前value的大小坪创,更新當(dāng)前緩存數(shù)據(jù)的size大锌簧簟;如果原來不存在該key莱预,則直接加入緩存即可柠掂;
- 清理緩存空間,當(dāng)我們加入一個(gè)數(shù)據(jù)時(shí)(put)依沮,為了保證當(dāng)前數(shù)據(jù)的緩存所占大小沒有超過我們指定的總大小涯贞,通過調(diào)用trimToSize()來對緩存空間進(jìn)行管理控制。
trimToSize
public void trimToSize(int maxSize) {
/*
* 循環(huán)進(jìn)行LRU悉抵,直到當(dāng)前所占容量大小沒有超過指定的總?cè)萘看笮? */
while (true) {
K key;
V value;
synchronized (this) {
// 一些異常情況的處理
if (size < 0 || (map.isEmpty() && size != 0)) {
throw new IllegalStateException(
getClass().getName() + ".sizeOf() is reporting inconsistent results!");
}
// 首先判斷當(dāng)前緩存數(shù)據(jù)大小是否超過了指定的緩存空間總大小肩狂。如果沒有超過摘完,即緩存中還可以存入數(shù)據(jù)姥饰,直接跳出循環(huán),清理完畢
if (size <= maxSize || map.isEmpty()) {
break;
}
/**
* 執(zhí)行到這孝治,表示當(dāng)前緩存數(shù)據(jù)已超過了總?cè)萘苛蟹啵枰獔?zhí)行LRU审磁,即將最近最少使用的數(shù)據(jù)清除掉,直到數(shù)據(jù)所占緩存空間沒有超標(biāo);
* 根據(jù)前面的原理分析岂座,知道态蒂,在鏈表中,鏈表的頭結(jié)點(diǎn)是最近最少使用的數(shù)據(jù)费什,因此钾恢,最先清除掉鏈表前面的結(jié)點(diǎn)
*/
Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
// 移除掉后,更新當(dāng)前數(shù)據(jù)緩存的大小
size -= safeSizeOf(key, value);
// 更新移除的結(jié)點(diǎn)數(shù)量
evictionCount++;
}
/*
* 通知某個(gè)結(jié)點(diǎn)被移除鸳址,類似于回調(diào)
*/
entryRemoved(true, key, value, null);
}
}
- trimToSize()方法的作用就是為了保證當(dāng)前數(shù)據(jù)的緩存大小不能超過我們指定的緩存總大小瘩蚪,如果超過了,就會開始移除最近最少使用的數(shù)據(jù)稿黍,直到size符合要求疹瘦。
- trimToSize()方法在put()的時(shí)候一定會調(diào)用,在get()的時(shí)候有可能會調(diào)用巡球。
public final V get(K key) {}獲取數(shù)據(jù)
/**
* 根據(jù)key查詢緩存言沐,如果該key對應(yīng)的value存在于緩存,直接返回value酣栈;
* 訪問到這個(gè)結(jié)點(diǎn)時(shí)险胰,LinkHashMap會將它移動(dòng)到雙向循環(huán)鏈表的的尾部。
* 如果如果沒有緩存的值矿筝,則返回null鸯乃。(如果開發(fā)者重寫了create()的話,返回創(chuàng)建的value)
*/
public final V get(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
// LinkHashMap 如果設(shè)置按照訪問順序的話跋涣,這里每次get都會重整數(shù)據(jù)順序
mapValue = map.get(key);
// 計(jì)算 命中次數(shù)
if (mapValue != null) {
hitCount++;
return mapValue;
}
// 計(jì)算 丟失次數(shù)
missCount++;
}
/*
* 官方解釋:
* 嘗試創(chuàng)建一個(gè)值缨睡,這可能需要很長時(shí)間,并且Map可能在create()返回的值時(shí)有所不同陈辱。如果在create()執(zhí)行的時(shí)
* 候奖年,用這個(gè)key執(zhí)行了put方法,那么此時(shí)就發(fā)生了沖突沛贪,我們在Map中刪除這個(gè)創(chuàng)建的值陋守,釋放被創(chuàng)建的值,保留put進(jìn)去的值利赋。
*/
V createdValue = create(key);
if (createdValue == null) {
return null;
}
/***************************
* 不覆寫create方法走不到下面 *
***************************/
/*
* 正常情況走不到這里
* 走到這里的話 說明 實(shí)現(xiàn)了自定義的 create(K key) 邏輯
* 因?yàn)槟J(rèn)的 create(K key) 邏輯為null
*/
synchronized (this) {
// 記錄 create 的次數(shù)
createCount++;
// 將自定義create創(chuàng)建的值水评,放入LinkedHashMap中,如果key已經(jīng)存在媚送,會返回 之前相同key 的值
mapValue = map.put(key, createdValue);
// 如果之前存在相同key的value中燥,即有沖突。
if (mapValue != null) {
/*
* 有沖突
* 所以 撤銷 剛才的 操作
* 將 之前相同key 的值 重新放回去
*/
map.put(key, mapValue);
} else {
// 拿到鍵值對塘偎,計(jì)算出在容量中的相對長度疗涉,然后加上
size += safeSizeOf(key, createdValue);
}
}
// 如果上面 判斷出了 將要放入的值發(fā)生沖突
if (mapValue != null) {
/*
* 剛才create的值被刪除了拿霉,原來的 之前相同key 的值被重新添加回去了
* 告訴 自定義 的 entryRemoved 方法
*/
entryRemoved(false, key, createdValue, mapValue);
return mapValue;
} else {
// 上面 進(jìn)行了 size += 操作 所以這里要重整長度
trimToSize(maxSize);
return createdValue;
}
}
- 先嘗試從map緩存中獲取value,即mapVaule = map.get(key);如果mapVaule != null咱扣,說明緩存中存在該對象绽淘,直接返回即可;
- 如果mapVaule == null闹伪,說明緩存中不存在該對象沪铭,大多數(shù)情況下會直接返回null;但是如果我們重寫了create()方法偏瓤,在緩存沒有該數(shù)據(jù)的時(shí)候自己去創(chuàng)建一個(gè)伦意,則會繼續(xù)往下走,中間可能會出現(xiàn)沖突硼补,看注釋驮肉;
- 注意:在我們通過LinkedHashMap進(jìn)行g(shù)et(key)或put(key,value)時(shí)都會對鏈表進(jìn)行調(diào)整,即將剛剛訪問get或加入put的結(jié)點(diǎn)放入到鏈表尾部已骇。
entryRemoved()
/**
* 1.當(dāng)被回收或者刪掉時(shí)調(diào)用离钝。該方法當(dāng)value被回收釋放存儲空間時(shí)被remove調(diào)用
* 或者替換條目值時(shí)put調(diào)用,默認(rèn)實(shí)現(xiàn)什么都沒做褪储。
* 2.該方法沒用同步調(diào)用卵渴,如果其他線程訪問緩存時(shí),該方法也會執(zhí)行鲤竹。
* 3.evicted=true:如果該條目被刪除空間 (表示 進(jìn)行了trimToSize or remove) evicted=false:put沖突后 或 get里成功create后
* 導(dǎo)致
* 4.newValue!=null浪读,那么則被put()或get()調(diào)用。
*/
protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {
}
- 可以發(fā)現(xiàn)entryRemoved方法是一個(gè)空方法辛藻,說明這個(gè)也是讓開發(fā)者自己根據(jù)需求去重寫的碘橘。
- entryRemoved()主要作用就是在結(jié)點(diǎn)數(shù)據(jù)value需要被刪除或回收的時(shí)候,給開發(fā)者的回調(diào)吱肌。開發(fā)者就可以在這個(gè)方法里面實(shí)現(xiàn)一些自己的邏輯:
- 可以進(jìn)行資源的回收痘拆;
- 可以實(shí)現(xiàn)二級內(nèi)存緩存,可以進(jìn)一步提高性能氮墨,思路如下:
二級緩存
- 重寫LruCache的entryRemoved()函數(shù)纺蛆,
- 把刪除掉的item,再次存入另外一個(gè)
LinkedHashMap<String, SoftWeakReference<Bitmap>>
中 - 這個(gè)數(shù)據(jù)結(jié)構(gòu)當(dāng)做二級緩存规揪,每次獲得圖片的時(shí)候桥氏,先判斷LruCache中是否緩存,沒有的話猛铅,再判斷這個(gè)二級緩存中是否有字支,如果都沒有再從sdcard上獲取。sdcard上也沒有的話,就從網(wǎng)絡(luò)服務(wù)器上拉取祥款。
entryRemoved()在LruCache中有四個(gè)地方進(jìn)行了調(diào)用:put()、get()月杉、trimToSize()刃跛、remove()中進(jìn)行了調(diào)用。
2. DiskLruCache
Android DiskLruCache 源碼解析 硬盤緩存的絕佳方案
原文地址
http://blog.csdn.net/shakespeare001/article/details/51695358