Volley HTTP 緩存規則
在介紹Volley的HTTP緩存機制之前,我們首先來看一下HTTP HEADER中和緩存有關的字段有:
規則 | 字段 | 示例值 | 類型 | 作用 |
---|---|---|---|---|
新鮮度 | Expires | Sat, 23 Jul 2016 03:34:17 GMT | 響應 | 告訴客戶端在過期時間之前可以使用副本 |
Cache-Control | no-cache | 響應 | 告訴客戶端忽略資源的緩存副本,強制每次請求都訪問服務器 | |
no-store | 響應 | 強制緩存在任何情況下都不要保留任何副本 | ||
must-revalidate | 響應 | 表示必須進行新鮮度的再驗證之後才能使用 | ||
max-age=[秒] | 響應 | 指明緩存副本的有效時長,從請求時間到到期時間的秒數 | ||
Last-Modified | Mon, 23 Jun 2014 08:43:26 GMT | 響應 | 告訴客戶端當前資源的最後修改時間 | |
If-Modified-Since | Mon, 23 Jun 2014 08:43:26 GMT | 請求 | 如果瀏覽器第一次請求時響應的Last-Modified非空,第二次請求同一資源時,會把它作爲該項的值發送給服務器 | |
校驗值 | ETag | 53a7e8ae-1f79 | 響應 | 告知客戶端當前資源在服務器的唯一標識 |
If-None-Match | 53a7e8ae-1f79 | 請求 | 如果瀏覽器第一次請求時響應中ETag非空,第二次請求同一資源時,會把它作爲該項的值發給服務器 |
Volley的緩存機制
Volley的緩存機制在對HTTP RESPONSE的解析中能夠明顯的看出來:
public static Cache.Entry parseCacheHeaders(NetworkResponse response) {
long now = System.currentTimeMillis();
Map<String, String> headers = response.headers;
long serverDate = 0;
long lastModified = 0;
long serverExpires = 0;
long softExpire = 0;
long finalExpire = 0;
long maxAge = 0;
long staleWhileRevalidate = 0;
boolean hasCacheControl = false;
boolean mustRevalidate = false;
String serverEtag;
String headerValue;
headerValue = headers.get("Date");
if (headerValue != null) {
serverDate = parseDateAsEpoch(headerValue);
}
// 獲取響應體的Cache緩存策略.
headerValue = headers.get("Cache-Control");
if (headerValue != null) {
hasCacheControl = true;
String[] tokens = headerValue.split(",");
for (String token : tokens) {
token = token.trim();
if (token.equals("no-cache") || token.equals("no-store")) {
// no-cache|no-store代表服務器禁止客戶端緩存,每次需要重新發送HTTP請求
return null;
} else if (token.startsWith("max-age=")) {
// 獲取緩存的有效時間
try {
maxAge = Long.parseLong(token.substring(8));
} catch (Exception e) {
maxAge = 0;
}
} else if (token.startsWith("stale-while-revalidate=")) {
try {
staleWhileRevalidate = Long.parseLong(token.substring(23));
} catch (Exception e) {
staleWhileRevalidate = 0;
}
} else if (token.equals("must-revalidate") || token.equals("proxy-revalidate")) {
// 需要進行新鮮度驗證
mustRevalidate = true;
}
}
}
// 獲取服務器資源的過期時間
headerValue = headers.get("Expires");
if (headerValue != null) {
serverExpires = parseDateAsEpoch(headerValue);
}
// 獲取服務器資源最後一次的修改時間
headerValue = headers.get("Last-Modified");
if (headerValue != null) {
lastModified = parseDateAsEpoch(headerValue);
}
// 獲取服務器資源標識
serverEtag = headers.get("ETag");
// 計算緩存的ttl和softTtl
if (hasCacheControl) {
softExpire = now + maxAge * 1000;
finalExpire = mustRevalidate
? softExpire
: softExpire + staleWhileRevalidate * 1000;
} else if (serverDate > 0 && serverExpires >= serverDate) {
// Default semantic for Expire header in HTTP specification is softExpire.
softExpire = now + (serverExpires - serverDate);
finalExpire = softExpire;
}
Cache.Entry entry = new Cache.Entry();
entry.data = response.data;
entry.etag = serverEtag;
entry.softTtl = softExpire;
entry.ttl = finalExpire;
entry.serverDate = serverDate;
entry.lastModified = lastModified;
entry.responseHeaders = headers;
return entry;
}
這個方法其實是實現了Volley的本地緩存的關鍵代碼.
L2級硬盤緩存的實現和緩存替換機制
之前介紹了用戶使用LruCache實現自定義的L1級緩存,而Volley本身利用了FIFO算法實現了L2級硬盤緩存.接下來,就詳細介紹一下硬盤緩存的實現和緩存替換機制.
這裏我們也是考慮如果自己實現硬盤緩存,需要實現哪幾個步驟:
- 抽象出存儲實體類.
- 定義抽象存儲接口,包括initialize,get,put,clear等具體緩存系統的操作.
- 對象的序列化.
存儲實體
存儲的實體肯定是響應的結果,響應結果分爲響應頭和響應體,抽象類代碼如下所示:
/** 真正HTTP請求緩存實體類. */
class Entry {
/** HTTP響應Headers. */
public Map<String, String> responseHeaders = Collections.emptyMap();
/** HTTP響應體. */
public byte[] data;
/** 服務器資源標識ETag. */
public String etag;
/** HTTP響應時間. */
public long serverDate;
/** 緩存內容最後一次修改的時間. */
public long lastModified;
/** Request的緩存過期時間. */
public long ttl;
/** Request的緩存新鮮時間. */
public long softTtl;
/** 判斷緩存內容是否過期. */
public boolean isExpired() {
return this.ttl < System.currentTimeMillis();
}
/** 判斷緩存是否新鮮,不新鮮的緩存需要發到服務端做新鮮度的檢測. */
public boolean refreshNeeded() {
return this.softTtl < System.currentTimeMillis();
}
}
抽象緩存系統類
public interface Cache {
/** 通過key獲取請求的緩存實體. */
Entry get(String key);
/** 存入一個請求的緩存實體. */
void put(String key, Entry entry);
void initialize();
void invalidate(String key, boolean fullExpire);
/** 移除指定的緩存實體. */
void remove(String key);
/** 清空緩存. */
void clear();
}
在Volley中,實現Cache接口的硬盤緩存類是DiskBasedCache.接下來,具體介紹每個方法的具體實現.
構造函數
我們先來看一下DiskBasedCache的構造函數實現:
public DiskBasedCache(File rootDirectory) {
this(rootDirectory, DEFAULT_DISK_USAGE_BYTES);
}
public DiskBasedCache(File rootDirectory, int maxCacheSizeInBytes) {
mRootDirectory = rootDirectory;
mMaxCacheSizeInBytes = maxCacheSizeInBytes;
}
類似於LruCache,DiskBasedCache的構造函數做了兩件事:
- 指定硬盤緩存的目錄.
- 指定硬盤緩存的大小,默認爲5M.
initialize函數
在介紹put和get函數之前,先介紹一下硬盤緩存的初始化函數,這個函數主要是用來遍歷緩存的文件,從而獲取當前緩存大小,和構造
/**
* 初始化Disk緩存系統.
* 作用是:遍歷Disk緩存系統,將緩存文件中的CacheHeader和key存儲到Map對象中.
*/
public void initialize() {
if (!mRootDirectory.exists() && !mRootDirectory.mkdirs()) {
// 硬盤緩存目錄不存在直接返回即可
return;
}
// 獲取硬盤緩存目錄所有文件集合.每個HTTP請求結果對應一個文件.
File[] files = mRootDirectory.listFiles();
if (files == null) {
return;
}
for (File file : files) {
BufferedInputStream fis = null;
try {
fis = new BufferedInputStream(new FileInputStream(file));
// 進行對象反序列化
CacheHeader entry = CacheHeader.readHeader(fis);
// 將文件的大小賦值給entry.size,單位字節
entry.size = file.length();
// 在內存中維護一張硬盤<key,value>映射表
putEntry(entry.key, entry);
}catch (IOException e) {
file.delete();
e.printStackTrace();
}finally {
if (fis != null) {
try {
fis.close();
} catch (IOException ignored) {
}
}
}
}
}
/** 將key和CacheHeader存入到Map對象中.並更新當前佔用的總字節數. */
private void putEntry(String key, CacheHeader entry) {
if (!mEntries.containsKey(key)) {
mTotalSize += entry.size;
} else {
CacheHeader oldEntry = mEntries.get(key);
mTotalSize += (entry.size - oldEntry.size);
}
mEntries.put(key, entry);
}
put函數
接下來我們講解put函數,是因爲一個緩存系統最爲關鍵的操作就是put,這其中還設計到緩存替換策略的實現.
首先是緩存替換策略.
private void pruneIfNeeded(int neededSpace) {
if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes) {
return;
}
Iterator<Map.Entry<String, CacheHeader>> iterator = mEntries.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, CacheHeader> entry = iterator.next();
CacheHeader e = entry.getValue();
// 這裏的替換策略不太好,其實可以按照serverDate排序,從而實現FIFO的緩存替換策略.
boolean deleted = getFileForKey(e.key).delete();
if (deleted) {
mTotalSize -= e.size;
}
iterator.remove();
// 當硬盤大小滿足可以存放新的HTTP請求結果時,停止刪除操作
if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes * HYSTERESIS_FACTOR) {
break;
}
}
}
接下來,是硬盤緩存的插入操作,準備是對象序列化的一些內容.
/** 將Cache.Entry存入到指定的緩存文件中. 並在Map中記錄<key,CacheHeader>. */
@Override
public synchronized void put(String key, Entry entry) {
pruneIfNeeded(entry.data.length);
// 根據HTTP的url生成緩存文件(ps:根據hash值生成文件名)
File file = getFileForKey(key);
try {
BufferedOutputStream fos = new BufferedOutputStream(new FileOutputStream(file));
// 這裏有個bug,插入時的size只計算響應體,沒有考慮響應頭部緩存字段的大小.
CacheHeader e = new CacheHeader(key, entry);
boolean success = e.writeHeader(fos);
if (!success) {
fos.close();
throw new IOException();
}
fos.write(entry.data);
fos.close();
putEntry(key, e);
return;
} catch (IOException e) {
e.printStackTrace();
}
file.delete();
}
get函數
get函數比較簡單了,源碼如下:
/** 從Disk中根據key獲取並構造HTTP響應體Cache.Entry. */
@Override
public synchronized Entry get(String key) {
CacheHeader entry = mEntries.get(key);
if (entry == null) {
return null;
}
File file = getFileForKey(key);
CountingInputStream cis = null;
try {
cis = new CountingInputStream(new BufferedInputStream(new FileInputStream(file)));
// 讀完CacheHeader部分,並通過CountingInputStream的bytesRead成員記錄已經讀取的字節數.
CacheHeader.readHeader(cis);
// 讀取緩存文件存儲的HTTP響應體內容.
byte[] data = streamToBytes(cis, (int)(file.length() - cis.bytesRead));
return entry.toCacheEntry(data);
} catch (IOException e) {
remove(key);
return null;
} finally {
if (cis != null) {
try {
cis.close();
} catch (IOException ignored) {
}
}
}
}
clear函數
clear顧名思義,就是清空硬盤緩存的操作:
public synchronized void clear() {
File[] files = mRootDirectory.listFiles();
if (files != null) {
for (File file : files) {
file.delete();
}
}
mEntries.clear();
mTotalSize = 0;
}
所做的事情也比較簡單,包括:
- 情況緩存文件.
- 將使用size置爲0.
- 清空內存中維護的硬盤
remove函數
remove函數也就是刪除指定key對應的硬盤緩存,代碼很簡單:
@Override
public synchronized void remove(String key) {
boolean deleted = getFileForKey(key).delete();
removeEntry(key);
if (!deleted) {
Log.e("Volley", "沒能刪除key=" + key + ", 文件名=" + getFilenameForKey(key) + "緩存.");
}
}
/** 從Map對象中刪除key對應的鍵值對. */
private void removeEntry(String key) {
CacheHeader entry = mEntries.get(key);
if (entry != null) {
mTotalSize -= entry.size;
mEntries.remove(key);
}
}