Android緩存機制&一個緩存框架推薦


1、先推薦一個輕量級緩存框架——ACache(ASimpleCache)

ACache介紹:
ACache類似於SharedPreferences,但是比SharedPreferences功能更加強大,SharedPreferences只能保存一些基本數據類型、Serializable、Bundle等數據,
而Acache可以緩存如下數據:
普通的字符串、JsonObject、JsonArray、Bitmap、Drawable、序列化的java對象,和 byte數據。
主要特色:
  • 1:輕,輕到只有一個JAVA文件。
  • 2:可配置,可以配置緩存路徑,緩存大小,緩存數量等。
  • 3:可以設置緩存超時時間,緩存超時自動失效,並被刪除。
  • 4:支持多進程。
應用場景:
  • 1、替換SharePreference當做配置文件
  • 2、可以緩存網絡請求數據,比如oschina的android客戶端可以緩存http請求的新聞內容,緩存時間假設為1個小時,超時后自動失效,讓客戶端重新請求新的數據,減少客戶端流量,同時減少服務器並發量。
  • 3、您來說...
 

2、Android緩存機制

Android緩存分為內存緩存和文件緩存(磁盤緩存)。在早期,各大圖片緩存框架流行之前,常用的內存緩存方式是軟引用(SoftReference)和弱引用(WeakReference),如大部分的使用方式:HashMap<String url, SoftReference<Drawable>> imageCache;這種形式。從Android 2.3(Level 9)開始,垃圾回收器更傾向於回收SoftReference或WeakReference對象,這使得SoftReference和WeakReference變得不是那么實用有效。同時,到了Android 3.0(Level 11)之后,圖片數據Bitmap被放置到了內存的堆區域,而堆區域的內存是由GC管理的,開發者也就不需要進行圖片資源的釋放工作,但這也使得圖片數據的釋放無法預知,增加了造成OOM的可能。因此,在Android3.1以后,Android推出了LruCache這個內存緩存類,LruCache中的對象是強引用的。
 

2.1 內存緩存——LruCache源碼分析

    2.1.1 LRU

    LRU,全稱Least Rencetly Used,即最近最少使用,是一種非常常用的置換算法,也即淘汰最長時間未使用的對象。LRU在操作系統中的頁面置換算法中廣泛使用,我們的內存或緩存空間是有限的,當新加入一個對象時,造成我們的緩存空間不足了,此時就需要根據某種算法對緩存中原有數據進行淘汰貨刪除,而LRU選擇的是將最長時間未使用的對象進行淘汰。
    

   2.1.2 LruCache實現原理

    根據LRU算法的思想,要實現LRU最核心的是要有一種數據結構能夠基於訪問順序來保存緩存中的對象,這樣我們就能夠很方便的知道哪個對象是最近訪問的,哪個對象是最長時間未訪問的。LruCache選擇的是LinkedHashMap這個數據結構,LinkedHashMap是一個雙向循環鏈表,在構造LinkedHashMap時,通過一個boolean值來指定LinkedHashMap中保存數據的方式,LinkedHashMap的一個構造方法如下:    
[java]  view plain  copy
 
  1. /* 
  2.      * 初始化LinkedHashMap 
  3.      * 第一個參數:initialCapacity,初始大小 
  4.      * 第二個參數:loadFactor,負載因子=0.75f 
  5.      * 第三個參數:accessOrder=true,基於訪問順序;accessOrder=false,基於插入順序 
  6.      */  
  7.     public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {  
  8.         super(initialCapacity, loadFactor);  
  9.         init();  
  10.         this.accessOrder = accessOrder;  
  11.     }  
顯然,在LruCache中選擇的是accessOrder = true;此時,當accessOrder 設置為 true時,每當我們更新(即調用put方法)或訪問(即調用get方法)map中的結點時,LinkedHashMap內部都會將這個結點移動到鏈表的尾部,因此,在鏈表的尾部是最近剛剛使用的結點,在鏈表的頭部是是最近最少使用的結點,當我們的緩存空間不足時,就應該持續把鏈表頭部結點移除掉,直到有剩余空間放置新結點。
可以看到,LinkedHashMap完成了LruCache中的核心功能,那LruCache中剩下要做的就是定義緩存空間總容量,當前保存數據已使用的容量,對外提供put、get方法。
    

    2.1.3 LruCache源碼分析

    在了解了LruCache的核心原理之后,就可以開始分析LruCache的源碼了。
    (1)關鍵字段
    根據上面的分析,首先要有總容量、已使用容量、linkedHashMap這幾個關鍵字段,LruCache中提供了下面三個關鍵字段:    
[java]  view plain  copy
 
  1. //核心數據結構  
  2.     private final LinkedHashMap<K, V> map;  
  3.     // 當前緩存數據所占的大小  
  4.     private int size;  
  5.     //緩存空間總容量  
  6.     private int maxSize;  
      要注意的是size字段,因為map中可以存放各種類型的數據,這些數據的大小測量方式也是不一樣的,比如Bitmap類型的數據和String類型的數據計算他們的大小方式肯定不同,因此,LruCache中在計算放入數據大小的方法sizeOf中,只是簡單的返回了1,需要我們重寫這個方法,自己去定義數據的測量方式。因此,我們在使用LruCache的時候,經常會看到這種方式:    
[java]  view plain  copy
 
  1. private static final int CACHE_SIZE = 4 * 1024 * 1024;//4Mib  
  2.     LruCache<String,Bitmap> bitmapCache = new LruCache<String,Bitmap>(CACHE_SIZE){  
  3.         @Override  
  4.         protected int sizeOf(String key, Bitmap value) {  
  5.             return value.getByteCount();//自定義Bitmap數據大小的計算方式  
  6.         }  
  7.     };  
    (2)構造方法  
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);
}
    LruCache只有一個唯一的構造方法,在構造方法中,給定了緩存空間的總大小,初始化了LinkedHashMap核心數據結構,在LinkedHashMap中的第三個參數指定為true,也就設置了accessOrder=true,表示這個LinkedHashMap將是基於數據的訪問順序進行排序。
 
    (3)sizeOf()和safeSizeOf()方法
    根據上面的解釋,由於各種數據類型大小測量的標准不統一,具體測量的方法應該由使用者來實現,如上面給出的一個在實現LruCache時重寫sizeOf的一種常用實現方式。通過多態的性質,再具體調用sizeOf時會調用我們重寫的方法進行測量,LruCache對sizeOf()的調用進行一層封裝,如下:    
private int safeSizeOf(K key, V value) {
int result = sizeOf(key, value);
if (result < 0) {
throw new IllegalStateException("Negative size: " + key + "=" + value);
}
return result;
}
里面其實就是調用sizeOf()方法,返回sizeOf計算的大小。
上面就是LruCache的基本內容,下面就需要提供LruCache的核心功能了。
 
    (4)put方法緩存數據
    首先看一下它的源碼實現:    
/**
* 給對應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;
}
可以看到,put()方法主要有以下幾步:
1)key和value判空,說明LruCache中不允許key和value為null;
2)通過safeSizeOf()獲取要加入對象數據的大小,並更新當前緩存數據的大小;
3)將新的對象數據放入到緩存中,即調用LinkedHashMap的put方法,如果原來存在該key時,直接替換掉原來的value值,並返回之前的value值,得到之前value的大小,更新當前緩存數據的size大小;如果原來不存在該key,則直接加入緩存即可;
4)清理緩存空間,如下;
 
    (5)trimToSize()清理緩存空間
    當我們加入一個數據時(put),為了保證當前數據的緩存所占大小沒有超過我們指定的總大小,通過調用trimToSize()來對緩存空間進行管理控制。如下:
public void trimToSize(int maxSize) {
/*
* 循環進行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);
}
}
trimToSize()方法的作用就是為了保證當前數據的緩存大小不能超過我們指定的緩存總大小,如果超過了,就會開始移除最近最少使用的數據,直到size符合要求。trimToSize()方法在put()的時候一定會調用,在get()的時候有可能會調用。
 
    (6)get方法獲取緩存數據
   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;
}
}
   get()方法的思路就是:
 
   1)先嘗試從map緩存中獲取value,即mapVaule = map.get(key);如果mapVaule != null,說明緩存中存在該對象,直接返回即可;
   2)如果mapVaule == null,說明緩存中不存在該對象,大多數情況下會直接返回null;但是如果我們重寫了create()方法,在緩存沒有該數據的時候自己去創建一個,則會繼續往下走,中間可能會出現沖突,看注釋;
   3)注意:在我們通過LinkedHashMap進行get(key)或put(key,value)時都會對鏈表進行調整,即將剛剛訪問get或加入put的結點放入到鏈表尾部。
 
    (7)entryRemoved()
    entryRemoved的源碼如下:    
/**
* 1.當被回收或者刪掉時調用。該方法當value被回收釋放存儲空間時被remove調用
* 或者替換條目值時put調用,默認實現什么都沒做。
* 2.該方法沒用同步調用,如果其他線程訪問緩存時,該方法也會執行。
* 3.evicted=true:如果該條目被刪除空間 (表示 進行了trimToSize or removeevicted=falseput沖突后 或 get里成功create
* 導致
* 4.newValue!=null,那么則被put()get()調用。
*/
protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {
}
    可以發現entryRemoved方法是一個空方法,說明這個也是讓開發者自己根據需求去重寫的。entryRemoved()主要作用就是在結點數據value需要被刪除或回收的時候,給開發者的回調。開發者就可以在這個方法里面實現一些自己的邏輯:
(1)可以進行資源的回收;
(2)可以實現二級內存緩存,可以進一步提高性能,思路如下:重寫LruCache的entryRemoved()函數,把刪除掉的item,再次存入另外一個LinkedHashMap<String, SoftWeakReference<Bitmap>>中,這個數據結構當做二級緩存,每次獲得圖片的時候,先判斷LruCache中是否緩存,沒有的話,再判斷這個二級緩存中是否有,如果都沒有再從sdcard上獲取。sdcard上也沒有的話,就從網絡服務器上拉取。
    entryRemoved()在LruCache中有四個地方進行了調用:put()、get()、trimToSize()、remove()中進行了調用。
 
    (8)LruCache的線程安全性    
    LruCache是線程安全的,因為在put、get、trimToSize、remove的方法中都加入synchronized進行同步控制。
 

 2.1.4 LruCache的使用

上面就是整個LruCache中比較核心的的原理和方法,對於LruCache的使用者來說,我們其實主要注意下面幾個點:
(1)在構造LruCache時提供一個總的緩存大小;
(2)重寫sizeOf方法,對存入map的數據大小進行自定義測量;
(3)根據需要,決定是否要重寫entryRemoved()方法;
(4)使用LruCache提供的put和get方法進行數據的緩存
 
小結:
  • LruCache 自身並沒有釋放內存,只是 LinkedHashMap中將數據移除了,如果數據還在別的地方被引用了,還是有泄漏問題,還需要手動釋放內存;

  • 覆寫 entryRemoved 方法能知道 LruCache 數據移除是是否發生了沖突(沖突是指在map.put()的時候,對應的key中是否存在原來的值),也可以去手動釋放資源;

 

2.2 磁盤緩存(文件緩存)——DiskLruCache分析

    LruCache是一種內存緩存策略,但是當存在大量圖片的時候,我們指定的緩存內存空間可能很快就會用完,這個時候,LruCache就會頻繁的進行trimToSize()操作,不斷的將最近最少使用的數據移除,當再次需要該數據時,又得從網絡上重新加載。為此,Google提供了一種磁盤緩存的解決方案——DiskLruCache(DiskLruCache並沒有集成到Android源碼中,在Android Doc的例子中有講解)。

    2.2.1 DiskLruCache實現原理

    我們可以先來直觀看一下,使用了DiskLruCache緩存策略的APP,緩存目錄中是什么樣子,如下圖:
    
    可以看到,緩存目錄中有一堆文件名很長的文件,這些文件就是我們緩存的一張張圖片數據,在最后有一個文件名journal的文件,這個journal文件是DiskLruCache的一個日志文件,即保存着每張緩存圖片的操作記錄,journal文件正是實現DiskLruCache的核心。看到出現了journal文件,基本可以說明這個APP使用了DiskLruCache緩存策略。
    根據對LruCache的分析,要實現LRU,最重要的是要有一種數據結構能夠基於訪問順序來保存緩存中的對象,LinkedHashMap是一種非常合適的數據結構,為此,DiskLruCache也選擇了LinkedHashMap作為維護訪問順序的數據結構,但是,對於DiskLruCache來說,單單LinkedHashMap是不夠的,因為我們不能像LruCache一樣,直接將數據放置到LinkedHashMap的value中,也就是處於內存當中,在DiskLruCache中,數據是緩存到了本地文件,這里的LinkedHashMap中的value只是保存的是value的一些簡要信息Entry,如唯一的文件名稱、大小、是否可讀等信息,如:
 
         
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");
}
}
DiskLruCache中對於LinkedHashMap定義如下:
private final LinkedHashMap<String, Entry> lruEntries
= new LinkedHashMap<String, Entry>(0, 0.75f, true);
在LruCache中,由於數據是直接緩存中內存中,map中數據的建立是在使用LruCache緩存的過程中逐步建立的,而對於DiskLruCache,由於數據是緩存在本地文件,相當於是持久保存下來的一個文件,即使程序退出文件還在,因此,map中數據的建立,除了在使用DiskLruCache過程中建立外,map還應該包括之前已經存在的緩存文件,因此,在獲取DiskLruCache的實例時,DiskLruCache會去讀取journal這個日志文件,根據這個日志文件中的信息,建立map的初始數據,同時,會根據journal這個日志文件,維護本地的緩存文件。構造DiskLruCache的方法如下:
 
         
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;
}
其中,
cache.readJournal();    
cache.processJournal();
正是去讀取journal日志文件,建立起map中的初始數據,同時維護緩存文件。
 
那journal日志文件到底保存了什么信息呢,一個標准的journal日志文件信息如下:
libcore.io.DiskLruCache    //第一行,固定內容,聲明
1                                        //第二行,cache的版本號,恆為1
1                                        //第三行,APP的版本號
2                                        //第四行,一個key,可以存放多少條數據valueCount    
                                           //第五行,空行分割行
DIRTY 335c4c6028171cfddfbaae1a9c313c52
CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934
REMOVE 335c4c6028171cfddfbaae1a9c313c52
DIRTY 1ab96a171faeeee38496d8b330771a7a
CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
READ 335c4c6028171cfddfbaae1a9c313c52
READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
 
前五行稱為journal日志文件的頭,下面部分的每一行會以四種前綴之一開始:DIRTY、CLEAN、REMOVE、READ。

以一個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()方法來獲取實例,如下所示: 

  public Editor edit(String key) throws IOException

 

在寫入完成后,需要進行commit()。如下一個簡單示例:
[java]  view plain  copy
 
  1. new Thread(new Runnable() {    
  2.     @Override    
  3.     public void run() {    
  4.         try {    
  5.             String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg";    
  6.             String key = hashKeyForDisk(imageUrl);  //MD5對url進行加密,這個主要是為了獲得統一的16位字符  
  7.             DiskLruCache.Editor editor = mDiskLruCache.edit(key);  //拿到Editor,往journal日志中寫入DIRTY記錄  
  8.             if (editor != null) {    
  9.                 OutputStream outputStream = editor.newOutputStream(0);    
  10.                 if (downloadUrlToStream(imageUrl, outputStream)) {  //downloadUrlToStream方法為下載圖片的方法,並且將輸出流放到outputStream  
  11.                     editor.commit();  //完成后記得commit(),成功后,再往journal日志中寫入CLEAN記錄  
  12.                 } else {    
  13.                     editor.abort();  //失敗后,要remove緩存文件,往journal文件中寫入REMOVE記錄  
  14.                 }    
  15.             }    
  16.             mDiskLruCache.flush();  //將緩存操作同步到journal日志文件,不一定要在這里就調用  
  17.         } catch (IOException e) {    
  18.             e.printStackTrace();    
  19.         }    
  20.     }    
  21. }).start();   
注意每次調用edit()時,會向journal日志文件寫入DIRTY為前綴的一條記錄;文件保存成功后,調用commit()時,也會向journal日志中寫入一條CLEAN為前綴的一條記錄,如果失敗,需要調用abort(),abort()里面會向journal文件寫入一條REMOVE為前綴的記錄。
 
獲取緩存數據是通過get()方法實現的,如下一個簡單示例:
[java]  view plain  copy
 
  1. try {    
  2.     String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg";    
  3.     String key = hashKeyForDisk(imageUrl);  //MD5對url進行加密,這個主要是為了獲得統一的16位字符  
  4.      //通過get拿到value的Snapshot,里面封裝了輸入流、key等信息,調用get會向journal文件寫入READ為前綴的記錄  
  5.     DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);   
  6.     if (snapShot != null) {    
  7.         InputStream is = snapShot.getInputStream(0);    
  8.         Bitmap bitmap = BitmapFactory.decodeStream(is);    
  9.         mImage.setImageBitmap(bitmap);    
  10.     }    
  11. catch (IOException e) {    
  12.     e.printStackTrace();    
  13. }   
 
3)合適的地方進行flush()
在上面進行數據緩存或獲取緩存的時候,調用不同的方法會往journal中寫入不同前綴的一行記錄,記錄寫入是通過IO下的Writer寫入的,要真正生效,還需要調用writer的flush()方法,而DiskLruCache中的flush()方法中封裝了writer.flush()的操作,因此,我們只需要在合適地方調用DiskLruCache中的flush()方法即可。其作用也就是將操作記錄同步到journal文件中,這是一個消耗效率的IO操作,我們不用每次一往journal中寫數據后就調用flush,這樣對效率影響較大,可以在Activity的onPause()中調用一下即可。
 
小結&注意:
(1)我們可以在在UI線程中檢測內存緩存,即主線程中可以直接使用LruCache;
(2)使用DiskLruCache時,由於緩存或獲取都需要對本地文件進行操作,因此需要另開一個線程,在子線程中檢測磁盤緩存、保存緩存數據,磁盤操作從來不應該在UI線程中實現;
(3)LruCache內存緩存的核心是LinkedHashMap,而DiskLruCache的核心是LinkedHashMap和journal日志文件,相當於把journal看作是一塊“內存”,LinkedHashMap的value只保存文件的簡要信息,對緩存文件的所有操作都會記錄在journal日志文件中。
 
DiskLruCache可能的優化方案:
    DiskLruCache是基於日志文件journal的,這就決定了每次對緩存文件的操作都需要進行日志文件的記錄,我們可以不用journal文件,在第一次構造DiskLruCache的時候,直接從程序訪問緩存目錄下的緩存文件,並將每個緩存文件的訪問時間作為初始值記錄在map的value中,每次訪問或保存緩存都更新相應key對應的緩存文件的訪問時間,這樣就避免了頻繁的IO操作,這種情況下就需要使用單例模式對DiskLruCache進行構造了,上面的Acache輕量級的數據緩存類就是這種實現方式。
 

2.3 二級緩存

    LruCache內存緩存在解決數據量不是很大的情況下效果不錯,當數據很大時,比圖需要加載大量圖片,LruCache指定的緩存容量可能很快被耗盡,此時LruCache頻繁的替換移除淘汰文件,又頻繁要進行網絡請求,很有可能出現OOM,為此,在大量數據的情況下,我們可以將磁盤緩存DiskLruCache作為一個二級緩存的模式,優化緩存方案。
    流程就是,
    (1)當我們需要緩存數據的時候,既在內存中緩存,也將文件緩存到磁盤;
    (2)當獲取緩存文件時,先嘗試從內存緩存中獲取,如果存在,則直接返回該文件;如果不存在,則從磁盤緩存中獲取,如果磁盤緩存中還沒有,那就只能從網絡獲取,獲取到數據后,同時在內存和磁盤中進行緩存。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM