
前言
最近在刷面試題,遇到一個(gè)問(wèn)題,關(guān)于緩存的原理的,所以在這里幾個(gè)筆記,關(guān)于緩存很多大牛都說(shuō)過(guò)了,我只是做個(gè)筆記,下面的很多都是網(wǎng)上查看到的,并非原創(chuàng)
目錄
-
一:Android 緩存策略
- 內(nèi)存緩存(LruCache)
- 2.磁盤(pán)緩存(文件緩存)——DiskLruCache分析
- 3 ASimpleCache
-
二:使用
- LRU使用
- DiskLruCache
-
三 源碼分析
- LRU 源碼分析
一:Android 緩存策略
1. 內(nèi)存緩存(LruCache)
LRU,全稱Least Rencetly Used,即最近最少使用,是一種非常常用的置換算法,也即淘汰最長(zhǎng)時(shí)間未使用的對(duì)象。LRU在操作系統(tǒng)中的頁(yè)面置換算法中廣泛使用,我們的內(nèi)存或緩存空間是有限的,當(dāng)新加入一個(gè)對(duì)象時(shí),造成我們的緩存空間不足了,此時(shí)就需要根據(jù)某種算法對(duì)緩存中原有數(shù)據(jù)進(jìn)行淘汰貨刪除,而LRU選擇的是將最長(zhǎng)時(shí)間未使用的對(duì)象進(jìn)行淘汰。
2. 磁盤(pán)緩存(文件緩存)——DiskLruCache分析
JakeWharton/DiskLruCache
不同于LruCache,LruCache是將數(shù)據(jù)緩存到內(nèi)存中去,而DiskLruCache是外部緩存,例如可以將網(wǎng)絡(luò)下載的圖片永久的緩存到手機(jī)外部存儲(chǔ)中去,并可以將緩存數(shù)據(jù)取出來(lái)使用,DiskLruCache不是google官方所寫(xiě),但是得到了官方推薦
3. ASimpleCache
二:使用
1. LRU使用
(1) 實(shí)例化
看下源碼
public class LruCache<K, V> {}
鍵值對(duì)的形式
下面是往上很多標(biāo)準(zhǔn)的寫(xiě)法,用于保存圖片
private void init_Lru() {
//設(shè)置LruCache緩存的大小,一般為當(dāng)前進(jìn)程可用容量的1/8。
int maxMemory = (int) (Runtime.getRuntime().totalMemory() / 1024);
int cacheSize = maxMemory / 8;
// 重寫(xiě)sizeOf方法,計(jì)算出要緩存的每張圖片的大小。
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
// 重寫(xiě)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)橐僮魍獠看鎯?chǔ),所以必須要先加上權(quán)限:
<!-- 在SDCard中創(chuàng)建與刪除文件權(quán)限 -->
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
<!-- 往SDCard寫(xiě)入數(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ù)說(shuō)明
File directory 數(shù)據(jù)的緩存地址
int appVersion 當(dāng)前應(yīng)用程序的版本號(hào)
- int valueCount 指定同一個(gè)key可以對(duì)應(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)行提交才能使寫(xiě)入生效
editor.commit();
//表示放棄此次寫(xiě)入
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è)方法會(huì)返回當(dāng)前緩存路徑下所有緩存數(shù)據(jù)的總字節(jié)數(shù),以byte為單位,如果應(yīng)用程序中需要在界面上顯示當(dāng)前緩存數(shù)據(jù)的總大小,就可以通過(guò)調(diào)用這個(gè)方法計(jì)算出來(lái)flush()
這個(gè)方法用于將內(nèi)存中的操作記錄同步到日志文件(也就是journal文件)當(dāng)中。這個(gè)方法非常重要,因?yàn)镈iskLruCache能夠正常工作的前提就是要依賴于journal文件中的內(nèi)容。前面在講解寫(xiě)入緩存操作的時(shí)候我有調(diào)用過(guò)一次這個(gè)方法,但其實(shí)并不是每次寫(xiě)入緩存都要調(diào)用一次flush()方法的,頻繁地調(diào)用并不會(huì)帶來(lái)任何好處,只會(huì)額外增加同步j(luò)ournal文件的時(shí)間。比較標(biāo)準(zhǔn)的做法就是在Activity的onPause()方法中去調(diào)用一次flush()方法就可以了
close()
這個(gè)方法用于將DiskLruCache關(guān)閉掉,是和open()方法對(duì)應(yīng)的一個(gè)方法。關(guān)閉掉了之后就不能再調(diào)用DiskLruCache中任何操作緩存數(shù)據(jù)的方法,通常只應(yīng)該在Activity的onDestroy()方法中去調(diào)用close()方法。delete()
這個(gè)方法用于將所有的緩存數(shù)據(jù)全部刪除,比如說(shuō)網(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è)是核心,用于儲(chǔ)存緩存的對(duì)象,
- 在LinkedHashMap中的第三個(gè)參數(shù)指定為true,表示這個(gè)LinkedHashMap將是基于數(shù)據(jù)的訪問(wèn)順序進(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ù)類(lèi)型大小測(cè)量的標(biāo)準(zhǔn)不統(tǒng)一,具體測(cè)量的方法應(yīng)該由使用者來(lái)實(shí)現(xiàn)
保存數(shù)據(jù)
下面代碼來(lái)自 http://blog.csdn.net/shakespeare001/article/details/51695358
/**
* 給對(duì)應(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++;
// 通過(guò)鍵值對(duì),計(jì)算出要保存對(duì)象value的大小,并更新當(dāng)前緩存大小
size += safeSizeOf(key, value);
/*
* 如果 之前存在key,用新的value覆蓋原來(lái)的數(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,減去原來(lái)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ù)大小超過(guò)了總?cè)萘縨axSize時(shí),才會(huì)真正去執(zhí)行LRU)
trimToSize(maxSize);
return previous;
}
- key和value判空,說(shuō)明LruCache中不允許key和value為null;
- 通過(guò)safeSizeOf()獲取要加入對(duì)象數(shù)據(jù)的大小,并更新當(dāng)前緩存數(shù)據(jù)的大??;
- 將新的對(duì)象數(shù)據(jù)放入到緩存中,即調(diào)用LinkedHashMap的put方法,如果原來(lái)存在該key時(shí),直接替換掉原來(lái)的value值,并返回之前的value值,得到之前value的大小,更新當(dāng)前緩存數(shù)據(jù)的size大??;如果原來(lái)不存在該key,則直接加入緩存即可;
- 清理緩存空間,當(dāng)我們加入一個(gè)數(shù)據(jù)時(shí)(put),為了保證當(dāng)前數(shù)據(jù)的緩存所占大小沒(méi)有超過(guò)我們指定的總大小,通過(guò)調(diào)用trimToSize()來(lái)對(duì)緩存空間進(jìn)行管理控制。
trimToSize
public void trimToSize(int maxSize) {
/*
* 循環(huán)進(jìn)行LRU,直到當(dāng)前所占容量大小沒(méi)有超過(guò)指定的總?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ù)大小是否超過(guò)了指定的緩存空間總大小。如果沒(méi)有超過(guò),即緩存中還可以存入數(shù)據(jù),直接跳出循環(huán),清理完畢
if (size <= maxSize || map.isEmpty()) {
break;
}
/**
* 執(zhí)行到這,表示當(dāng)前緩存數(shù)據(jù)已超過(guò)了總?cè)萘?,需要?zhí)行LRU,即將最近最少使用的數(shù)據(jù)清除掉,直到數(shù)據(jù)所占緩存空間沒(méi)有超標(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)被移除,類(lèi)似于回調(diào)
*/
entryRemoved(true, key, value, null);
}
}
- trimToSize()方法的作用就是為了保證當(dāng)前數(shù)據(jù)的緩存大小不能超過(guò)我們指定的緩存總大小,如果超過(guò)了,就會(huì)開(kāi)始移除最近最少使用的數(shù)據(jù),直到size符合要求。
- trimToSize()方法在put()的時(shí)候一定會(huì)調(diào)用,在get()的時(shí)候有可能會(huì)調(diào)用。
public final V get(K key) {}獲取數(shù)據(jù)
/**
* 根據(jù)key查詢緩存,如果該key對(duì)應(yīng)的value存在于緩存,直接返回value;
* 訪問(wèn)到這個(gè)結(jié)點(diǎn)時(shí),LinkHashMap會(huì)將它移動(dòng)到雙向循環(huán)鏈表的的尾部。
* 如果如果沒(méi)有緩存的值,則返回null。(如果開(kāi)發(fā)者重寫(xiě)了create()的話,返回創(chuàng)建的value)
*/
public final V get(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
// LinkHashMap 如果設(shè)置按照訪問(wèn)順序的話,這里每次get都會(huì)重整數(shù)據(jù)順序
mapValue = map.get(key);
// 計(jì)算 命中次數(shù)
if (mapValue != null) {
hitCount++;
return mapValue;
}
// 計(jì)算 丟失次數(shù)
missCount++;
}
/*
* 官方解釋:
* 嘗試創(chuàng)建一個(gè)值,這可能需要很長(zhǎng)時(shí)間,并且Map可能在create()返回的值時(shí)有所不同。如果在create()執(zhí)行的時(shí)
* 候,用這個(gè)key執(zhí)行了put方法,那么此時(shí)就發(fā)生了沖突,我們?cè)贛ap中刪除這個(gè)創(chuàng)建的值,釋放被創(chuàng)建的值,保留put進(jìn)去的值。
*/
V createdValue = create(key);
if (createdValue == null) {
return null;
}
/***************************
* 不覆寫(xiě)create方法走不到下面 *
***************************/
/*
* 正常情況走不到這里
* 走到這里的話 說(shuō)明 實(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)存在,會(huì)返回 之前相同key 的值
mapValue = map.put(key, createdValue);
// 如果之前存在相同key的value,即有沖突。
if (mapValue != null) {
/*
* 有沖突
* 所以 撤銷(xiāo) 剛才的 操作
* 將 之前相同key 的值 重新放回去
*/
map.put(key, mapValue);
} else {
// 拿到鍵值對(duì),計(jì)算出在容量中的相對(duì)長(zhǎng)度,然后加上
size += safeSizeOf(key, createdValue);
}
}
// 如果上面 判斷出了 將要放入的值發(fā)生沖突
if (mapValue != null) {
/*
* 剛才create的值被刪除了,原來(lái)的 之前相同key 的值被重新添加回去了
* 告訴 自定義 的 entryRemoved 方法
*/
entryRemoved(false, key, createdValue, mapValue);
return mapValue;
} else {
// 上面 進(jìn)行了 size += 操作 所以這里要重整長(zhǎng)度
trimToSize(maxSize);
return createdValue;
}
}
- 先嘗試從map緩存中獲取value,即mapVaule = map.get(key);如果mapVaule != null,說(shuō)明緩存中存在該對(duì)象,直接返回即可;
- 如果mapVaule == null,說(shuō)明緩存中不存在該對(duì)象,大多數(shù)情況下會(huì)直接返回null;但是如果我們重寫(xiě)了create()方法,在緩存沒(méi)有該數(shù)據(jù)的時(shí)候自己去創(chuàng)建一個(gè),則會(huì)繼續(xù)往下走,中間可能會(huì)出現(xiàn)沖突,看注釋;
- 注意:在我們通過(guò)LinkedHashMap進(jìn)行g(shù)et(key)或put(key,value)時(shí)都會(huì)對(duì)鏈表進(jìn)行調(diào)整,即將剛剛訪問(wèn)get或加入put的結(jié)點(diǎn)放入到鏈表尾部。
entryRemoved()
/**
* 1.當(dāng)被回收或者刪掉時(shí)調(diào)用。該方法當(dāng)value被回收釋放存儲(chǔ)空間時(shí)被remove調(diào)用
* 或者替換條目值時(shí)put調(diào)用,默認(rèn)實(shí)現(xiàn)什么都沒(méi)做。
* 2.該方法沒(méi)用同步調(diào)用,如果其他線程訪問(wèn)緩存時(shí),該方法也會(huì)執(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è)空方法,說(shuō)明這個(gè)也是讓開(kāi)發(fā)者自己根據(jù)需求去重寫(xiě)的。
- entryRemoved()主要作用就是在結(jié)點(diǎn)數(shù)據(jù)value需要被刪除或回收的時(shí)候,給開(kāi)發(fā)者的回調(diào)。開(kāi)發(fā)者就可以在這個(gè)方法里面實(shí)現(xiàn)一些自己的邏輯:
- 可以進(jìn)行資源的回收;
- 可以實(shí)現(xiàn)二級(jí)內(nèi)存緩存,可以進(jìn)一步提高性能,思路如下:
二級(jí)緩存
- 重寫(xiě)LruCache的entryRemoved()函數(shù),
- 把刪除掉的item,再次存入另外一個(gè)
LinkedHashMap<String, SoftWeakReference<Bitmap>>中 - 這個(gè)數(shù)據(jù)結(jié)構(gòu)當(dāng)做二級(jí)緩存,每次獲得圖片的時(shí)候,先判斷LruCache中是否緩存,沒(méi)有的話,再判斷這個(gè)二級(jí)緩存中是否有,如果都沒(méi)有再?gòu)膕dcard上獲取。sdcard上也沒(méi)有的話,就從網(wǎng)絡(luò)服務(wù)器上拉取。
entryRemoved()在LruCache中有四個(gè)地方進(jìn)行了調(diào)用:put()、get()、trimToSize()、remove()中進(jìn)行了調(diào)用。
2. DiskLruCache
Android DiskLruCache 源碼解析 硬盤(pán)緩存的絕佳方案
原文地址
http://blog.csdn.net/shakespeare001/article/details/51695358