1、先推薦一個輕量級緩存框架——ACache(ASimpleCache)
- 1:輕,輕到只有一個JAVA文件。
- 2:可配置,可以配置緩存路徑,緩存大小,緩存數量等。
- 3:可以設置緩存超時時間,緩存超時自動失效,並被刪除。
- 4:支持多進程。
- 1、替換SharePreference當做配置文件
- 2、可以緩存網絡請求數據,比如oschina的android客戶端可以緩存http請求的新聞內容,緩存時間假設為1個小時,超時后自動失效,讓客戶端重新請求新的數據,減少客戶端流量,同時減少服務器並發量。
- 3、您來說...
2、Android緩存機制
2.1 內存緩存——LruCache源碼分析
2.1.1 LRU
2.1.2 LruCache實現原理
- /*
- * 初始化LinkedHashMap
- * 第一個參數:initialCapacity,初始大小
- * 第二個參數:loadFactor,負載因子=0.75f
- * 第三個參數:accessOrder=true,基於訪問順序;accessOrder=false,基於插入順序
- */
- public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
- super(initialCapacity, loadFactor);
- init();
- this.accessOrder = accessOrder;
- }
2.1.3 LruCache源碼分析
- //核心數據結構
- private final LinkedHashMap<K, V> map;
- // 當前緩存數據所占的大小
- private int size;
- //緩存空間總容量
- private int maxSize;
- private static final int CACHE_SIZE = 4 * 1024 * 1024;//4Mib
- LruCache<String,Bitmap> bitmapCache = new LruCache<String,Bitmap>(CACHE_SIZE){
- @Override
- protected int sizeOf(String key, Bitmap value) {
- return value.getByteCount();//自定義Bitmap數據大小的計算方式
- }
- };
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);
}
private int safeSizeOf(K key, V value) {
int result = sizeOf(key, value);
if (result < 0) {
throw new IllegalStateException("Negative size: " + key + "=" + value);
}
return result;
}
/**
* 給對應key緩存value,並且將該value移動到鏈表的尾部。
*/
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 的次數
putCount++;
// 通過鍵值對,計算出要保存對象value的大小,並更新當前緩存大小
size += safeSizeOf(key, value);
/*
* 如果 之前存在key,用新的value覆蓋原來的數據, 並返回 之前key 的value
* 記錄在 previous
*/
previous = map.put(key, value);
// 如果之前存在key,並且之前的value不為null
if (previous != null) {
// 計算出 之前value的大小,因為前面size已經加上了新的value數據的大小,此時,需要再次更新size,減去原來value的大小
size -= safeSizeOf(key, previous);
}
}
// 如果之前存在key,並且之前的value不為null
if (previous != null) {
/*
* previous值被剔除了,此次添加的 value 已經作為key的 新值
* 告訴 自定義 的 entryRemoved 方法
*/
entryRemoved(false, key, previous, value);
}
//裁剪緩存容量(在當前緩存數據大小超過了總容量maxSize時,才會真正去執行LRU)
trimToSize(maxSize);
return previous;
}
public void trimToSize(int maxSize) {trimToSize()方法的作用就是為了保證當前數據的緩存大小不能超過我們指定的緩存總大小,如果超過了,就會開始移除最近最少使用的數據,直到size符合要求。trimToSize()方法在put()的時候一定會調用,在get()的時候有可能會調用。
/*
* 循環進行LRU,直到當前所占容量大小沒有超過指定的總容量大小
*/
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!");
}
// 首先判斷當前緩存數據大小是否超過了指定的緩存空間總大小。如果沒有超過,即緩存中還可以存入數據,直接跳出循環,清理完畢
if (size <= maxSize || map.isEmpty()) {
break;
}
/**
* 執行到這,表示當前緩存數據已超過了總容量,需要執行LRU,即將最近最少使用的數據清除掉,直到數據所占緩存空間沒有超標;
* 根據前面的原理分析,知道,在鏈表中,鏈表的頭結點是最近最少使用的數據,因此,最先清除掉鏈表前面的結點
*/
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);
}
}
/**get()方法的思路就是:
* 根據key查詢緩存,如果該key對應的value存在於緩存,直接返回value;
* 訪問到這個結點時,LinkHashMap會將它移動到雙向循環鏈表的的尾部。
* 如果如果沒有緩存的值,則返回null。(如果開發者重寫了create()的話,返回創建的value)
*/
public final V get(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
// LinkHashMap 如果設置按照訪問順序的話,這里每次get都會重整數據順序
mapValue = map.get(key);
// 計算 命中次數
if (mapValue != null) {
hitCount++;
return mapValue;
}
// 計算 丟失次數
missCount++;
}
/*
* 官方解釋:
* 嘗試創建一個值,這可能需要很長時間,並且Map可能在create()返回的值時有所不同。如果在create()執行的時
* 候,用這個key執行了put方法,那么此時就發生了沖突,我們在Map中刪除這個創建的值,釋放被創建的值,保留put進去的值。
*/
V createdValue = create(key);
if (createdValue == null) {
return null;
}
/***************************
* 不覆寫create方法走不到下面 *
***************************/
/*
* 正常情況走不到這里
* 走到這里的話 說明 實現了自定義的 create(K key) 邏輯
* 因為默認的 create(K key) 邏輯為null
*/
synchronized (this) {
// 記錄 create 的次數
createCount++;
// 將自定義create創建的值,放入LinkedHashMap中,如果key已經存在,會返回 之前相同key 的值
mapValue = map.put(key, createdValue);
// 如果之前存在相同key的value,即有沖突。
if (mapValue != null) {
/*
* 有沖突
* 所以 撤銷 剛才的 操作
* 將 之前相同key 的值 重新放回去
*/
map.put(key, mapValue);
} else {
// 拿到鍵值對,計算出在容量中的相對長度,然后加上
size += safeSizeOf(key, createdValue);
}
}
// 如果上面 判斷出了 將要放入的值發生沖突
if (mapValue != null) {
/*
* 剛才create的值被刪除了,原來的 之前相同key 的值被重新添加回去了
* 告訴 自定義 的 entryRemoved 方法
*/
entryRemoved(false, key, createdValue, mapValue);
return mapValue;
} else {
// 上面 進行了 size += 操作 所以這里要重整長度
trimToSize(maxSize);
return createdValue;
}
}
/**
* 1.當被回收或者刪掉時調用。該方法當value被回收釋放存儲空間時被remove調用
* 或者替換條目值時put調用,默認實現什么都沒做。
* 2.該方法沒用同步調用,如果其他線程訪問緩存時,該方法也會執行。
* 3.evicted=true:如果該條目被刪除空間 (表示 進行了trimToSize or remove) evicted=false:put沖突后 或 get里成功create后
* 導致
* 4.newValue!=null,那么則被put()或get()調用。
*/
protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {
}
2.1.4 LruCache的使用
-
LruCache 自身並沒有釋放內存,只是 LinkedHashMap中將數據移除了,如果數據還在別的地方被引用了,還是有泄漏問題,還需要手動釋放內存;
-
覆寫
entryRemoved
方法能知道 LruCache 數據移除是是否發生了沖突(沖突是指在map.put()的時候,對應的key中是否存在原來的值),也可以去手動釋放資源;
2.2 磁盤緩存(文件緩存)——DiskLruCache分析
2.2.1 DiskLruCache實現原理

private final class Entry {
private final String key;
/** Lengths of this entry's files. */
private final long[] lengths;
/** True if this entry has ever been published */
private boolean readable;
/** The ongoing edit or null if this entry is not being edited. */
private Editor currentEditor;
/** The sequence number of the most recently committed edit to this entry. */
private long sequenceNumber;
private Entry(String key) {
this.key = key;
this.lengths = new long[valueCount];
}
public String getLengths() throws IOException {
StringBuilder result = new StringBuilder();
for (long size : lengths) {
result.append(' ').append(size);
}
return result.toString();
}
/**
* Set lengths using decimal numbers like "10123".
*/
private void setLengths(String[] strings) throws IOException {
if (strings.length != valueCount) {
throw invalidLengths(strings);
}
try {
for (int i = 0; i < strings.length; i++) {
lengths[i] = Long.parseLong(strings[i]);
}
} catch (NumberFormatException e) {
throw invalidLengths(strings);
}
}
private IOException invalidLengths(String[] strings) throws IOException {
throw new IOException("unexpected journal line: " + Arrays.toString(strings));
}
public File getCleanFile(int i) {
return new File(directory, key + "." + i);
}
public File getDirtyFile(int i) {
return new File(directory, key + "." + i + ".tmp");
}
}
private final LinkedHashMap<String, Entry> lruEntries在LruCache中,由於數據是直接緩存中內存中,map中數據的建立是在使用LruCache緩存的過程中逐步建立的,而對於DiskLruCache,由於數據是緩存在本地文件,相當於是持久保存下來的一個文件,即使程序退出文件還在,因此,map中數據的建立,除了在使用DiskLruCache過程中建立外,map還應該包括之前已經存在的緩存文件,因此,在獲取DiskLruCache的實例時,DiskLruCache會去讀取journal這個日志文件,根據這個日志文件中的信息,建立map的初始數據,同時,會根據journal這個日志文件,維護本地的緩存文件。構造DiskLruCache的方法如下:
= new LinkedHashMap<String, Entry>(0, 0.75f, true);
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)其中,
throws IOException {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
if (valueCount <= 0) {
throw new IllegalArgumentException("valueCount <= 0");
}
// prefer to pick up where we left off
DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
if (cache.journalFile.exists()) {
try {
cache.readJournal();
cache.processJournal();
cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true),IO_BUFFER_SIZE);
return cache;
} catch (IOException journalIsCorrupt) {
// System.logW("DiskLruCache " + directory + " is corrupt: "
// + journalIsCorrupt.getMessage() + ", removing");
cache.delete();
}
}
// create a new empty cache
directory.mkdirs();
cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
cache.rebuildJournal();
return cache;
}
以一個DIRTY前綴開始的,后面緊跟着緩存圖片的key。以DIRTY這個這個前綴開頭,意味着這是一條臟數據。每當我們調用一次DiskLruCache的edit()方法時,都會向journal文件中寫入一條DIRTY記錄,表示我們正准備寫入一條緩存數據,但不知結果如何。然后調用commit()方法表示寫入緩存成功,這時會向journal中寫入一條CLEAN記錄,意味着這條“臟”數據被“洗干凈了”,調用abort()方法表示寫入緩存失敗,這時會向journal中寫入一條REMOVE記錄。也就是說,每一行DIRTY的key,后面都應該有一行對應的CLEAN或者REMOVE的記錄,否則這條數據就是“臟”的,會被自動刪除掉。
在CLEAN前綴和key后面還有一個數值,代表的是該條緩存數據的大小。
因此,我們可以總結DiskLruCache中的工作流程:
1)初始化:通過open()方法,獲取DiskLruCache的實例,在open方法中通過readJournal(); 方法讀取journal日志文件,根據journal日志文件信息建立map中的初始數據;然后再調用processJournal();方法對剛剛建立起的map數據進行分析,分析的工作,一個是計算當前有效緩存文件(即被CLEAN的)的大小,一個是清理無用緩存文件;
2)數據緩存與獲取緩存:上面的初始化工作完成后,我們就可以在程序中進行數據的緩存功能和獲取緩存的功能了;
緩存數據的操作是借助DiskLruCache.Editor這個類完成的,這個類也是不能new的,需要調用DiskLruCache的edit()方法來獲取實例,如下所示:
在寫入完成后,需要進行commit()。如下一個簡單示例:
- new Thread(new Runnable() {
- @Override
- public void run() {
- try {
- String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg";
- String key = hashKeyForDisk(imageUrl); //MD5對url進行加密,這個主要是為了獲得統一的16位字符
- DiskLruCache.Editor editor = mDiskLruCache.edit(key); //拿到Editor,往journal日志中寫入DIRTY記錄
- if (editor != null) {
- OutputStream outputStream = editor.newOutputStream(0);
- if (downloadUrlToStream(imageUrl, outputStream)) { //downloadUrlToStream方法為下載圖片的方法,並且將輸出流放到outputStream
- editor.commit(); //完成后記得commit(),成功后,再往journal日志中寫入CLEAN記錄
- } else {
- editor.abort(); //失敗后,要remove緩存文件,往journal文件中寫入REMOVE記錄
- }
- }
- mDiskLruCache.flush(); //將緩存操作同步到journal日志文件,不一定要在這里就調用
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }).start();
- try {
- String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg";
- String key = hashKeyForDisk(imageUrl); //MD5對url進行加密,這個主要是為了獲得統一的16位字符
- //通過get拿到value的Snapshot,里面封裝了輸入流、key等信息,調用get會向journal文件寫入READ為前綴的記錄
- DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
- if (snapShot != null) {
- InputStream is = snapShot.getInputStream(0);
- Bitmap bitmap = BitmapFactory.decodeStream(is);
- mImage.setImageBitmap(bitmap);
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
2.3 二級緩存