在上一篇文章中,帶你們閱讀了 Volley 網絡請求的執行流程,算是對 Volley 有了一個比較清晰的認識,從這篇文章開始,咱們開始針對 Volley 的某個功能進行深刻地分析,慢慢將 Volley 的各項功能進行全面把握。算法
咱們先從緩存這一塊的內容開始入手,不過今天的緩存分析是是創建在上一篇源碼分析的基礎上的,尚未看過上一篇文章的朋友,建議先去閱讀 Android Volley 源碼解析(一),網絡請求的執行流程。緩存
在開始細節分析以前,咱們先來看下 Volley 緩存的設計,瞭解這個流程有助於咱們對於緩存細節的把握。Volley 提供了一個 Cache 做爲緩存的接口,封裝了緩存的實體 Entry,以及一些常規的增刪查操做。安全
public interface Cache {
Entry get(String key);
void put(String key, Entry entry);
void initialize();
/**
* 使緩存中的 Entry 失效
*/
void invalidate(String key, boolean fullExpire);
void remove(String key);
void clear();
/**
* 用戶緩存的實體
*/
class Entry {
public byte[] data;
public String etag;
public long serverDate;
public long lastModified;
public long ttl;
public long softTtl;
public Map<String, String> responseHeaders = Collections.emptyMap();
public List<Header> allResponseHeaders;
/** 判斷 Entry 是否過時. */
public boolean isExpired() {
return this.ttl < System.currentTimeMillis();
}
/** 判斷 Entry 是否須要刷新. */
public boolean refreshNeeded() {
return this.softTtl < System.currentTimeMillis();
}
}
}
複製代碼
Entry 裏面主要是放網絡響應的原始數據 data、跟緩存相關的屬性以及對應的響應頭,做爲緩存的一個實體。Cache 的具體實現類是 DiskBaseCache,它實現了 Cache 接口,並實現了響應的方法,那咱們就來看看 DiskBaseCache 的設計吧,咱們先看下 DiskBaseCache 中的一個靜態內部類 CacheHeader.bash
static class CacheHeader {
long size;
final String key;
final String etag;
final long serverDate;
final long lastModified;
final long ttl;
final long softTtl;
final List<Header> allResponseHeaders;
private CacheHeader(String key, String etag, long serverDate, long lastModified, long ttl,
long softTtl, List<Header> allResponseHeaders) {
this.key = key;
this.etag = ("".equals(etag)) ? null : etag;
this.serverDate = serverDate;
this.lastModified = lastModified;
this.ttl = ttl;
this.softTtl = softTtl;
this.allResponseHeaders = allResponseHeaders;
}
CacheHeader(String key, Entry entry) {
this(key, entry.etag, entry.serverDate, entry.lastModified, entry.ttl, entry.softTtl,
getAllResponseHeaders(entry));
size = entry.data.length;
}
}
複製代碼
DiskBaseCache 的設計很巧妙,它在內部放入了一個靜態內部類 CacheHeader,咱們能夠發現這個類跟 Cache 的 Entry 很是像,是否是會以爲好像有點多餘,Volley 之因此要這樣設計,主要是爲了緩存的合理性。咱們知道每個應用都是有必定內存限制的,程序佔用了太高的內存就容易出現 OOM(Out of Memory),若是每個請求都原封不動的把全部的信息都緩存到內存中,這樣是很是佔內存的。網絡
咱們能夠發現 CacheHeader 和 Entry 最大的區別,其實就是是否有 byte[] data 這個屬性,data 表明網絡響應的元數據,是返回的內容中最佔地方的東西,因此 DiskBaseCache 從新抽象了一個不包含 data 的 CacheHeader,並將其緩存到內存中,而 data 部分便存儲在磁盤緩存中,這樣就能最大程度的利用有限的內存空間。代碼以下:ide
BufferedOutputStream fos = new BufferedOutputStream(createOutputStream(file));
CacheHeader e = new CacheHeader(key, entry);
boolean success = e.writeHeader(fos);
// 將 entry.data 寫入磁盤中
fos.write(entry.data);
fos.close();
// 將 Cache 緩存到內存中
putEntry(key, e);
複製代碼
看完了 Volley 的緩存設計,咱們接着看 DiskBaseCache 的具體實現。源碼分析
// 內存緩存的目錄
private final File mRootDirectory;
public DiskBasedCache(File rootDirectory, int maxCacheSizeInBytes) {
mRootDirectory = rootDirectory;
mMaxCacheSizeInBytes = maxCacheSizeInBytes;
}
@Override
public synchronized void initialize() {
// 若是 mRootDirectroy 不存在,則進行建立
if (!mRootDirectory.exists()) {
if (!mRootDirectory.mkdirs()) {
VolleyLog.e("Unable to create cache dir %s", mRootDirectory.getAbsolutePath());
}
return;
}
File[] files = mRootDirectory.listFiles();
if (files == null) {
return;
}
// 遍歷 mRootDirectory 中的全部文件
for (File file : files) {
try {
long entrySize = file.length();
CountingInputStream cis = new CountingInputStream(
new BufferedInputStream(createInputStream(file)), entrySize);
// 將對應的文件緩存到內存中
CacheHeader entry = CacheHeader.readHeader(cis);
entry.size = entrySize;
putEntry(entry.key, entry);
} catch (IOException e) {
file.delete();
}
}
}
複製代碼
經過外部傳入的 rootDirectory 和 maxCacheSizeInBytes 構造 DiskBaseCache 的實例,mRootDirectory 表明咱們內存緩存的目錄,maxCacheSizeInBytes 表明磁盤緩存的大小,默認是 5M。若是 mRootDirectory 爲 null,則進行建立,而後將 mRootDirectory 中的全部文件進行內存緩存。post
@Override
public synchronized void put(String key, Entry entry) {
pruneIfNeeded(entry.data.length);
File file = getFileForKey(key);
try {
BufferedOutputStream fos = new BufferedOutputStream(createOutputStream(file));
CacheHeader e = new CacheHeader(key, entry);
boolean success = e.writeHeader(fos);
fos.write(entry.data);
fos.close();
putEntry(key, e);
return;
} catch (IOException e) {
}
}
private void pruneIfNeeded(int neededSpace) {
// 若是內存還夠用,就直接 return.
if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes) {
return;
}
long before = mTotalSize;
int prunedFiles = 0;
Iterator<Map.Entry<String, CacheHeader>> iterator = mEntries.entrySet().iterator();
// 遍歷全部的文件,開始進行刪除文件
while (iterator.hasNext()) {
Map.Entry<String, CacheHeader> entry = iterator.next();
CacheHeader e = entry.getValue();
boolean deleted = getFileForKey(e.key).delete();
if (deleted) {
mTotalSize -= e.size;
}
iterator.remove();
prunedFiles++;
// 若是刪除文件後,存儲空間已經夠用了,就中止循環
if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes * HYSTERESIS_FACTOR) {
break;
}
}
}
複製代碼
能夠看到 Volley 的代碼實現是至關完善的,在添加緩存以前,先調用 pruneIfNeed() 方法進行內存空間的判斷和處理,若是不進行限制的話,內存佔用將無限制的增大,最後到達 SD 卡容量時,會發生沒法寫入的異常(由於存儲空間滿了)。學習
這裏有一點要補充一下,Volley 在緩存方面,主要是使用了 LRU(Least Recently Used)算法,LRU 算法是最近最少使用算法,它的核心思想是當緩存滿時,優先淘汰那些近期最少使用的緩存對象。主要的算法原理是把最近使用的對象用強引用的方式(即咱們日常使用的對象引用方式)存儲在 LinkedHashMap 中,當緩存滿時,把最近最少使用的對象從內存中移除。有關 LRU 算法,能夠看下這篇文章:完全解析 Android 緩存機制 —— LruCache。ui
在進行內存空間的判斷以後,便將 entry.data 保存在磁盤中,將 CacheHeader 緩存在內存中,這樣 DiskBaseCache 的 put() 方法就完成了。
既然是緩存功能,必然有用於進行緩存的 key,咱們來看下 Volley 的緩存 key 是怎麼生成的。
private String getFilenameForKey(String key) {
int firstHalfLength = key.length() / 2;
String localFilename = String.valueOf(key.substring(0, firstHalfLength).hashCode());
localFilename += String.valueOf(key.substring(firstHalfLength).hashCode());
return localFilename;
}
複製代碼
Volley 的緩存 key 的生成方法仍是很騷的,將網絡請求的 Url 分紅兩半,而後將這兩部分的 hashCode 拼接成緩存 key。Volley 之因此要這樣作,主要是爲了儘可能避免 hashCode 重複形成的文件名重複,求兩次 hashCode 都與另一個 Url 相同的機率比只求一次要小不少,不過幾率小不表明不存在,可是 Java 在計算 hashCode 的速度是很是快的,這應該是 Volley 在權衡了安全性和效率以後作出的決定,這個思想是很值得咱們學習的。
@Override
public synchronized Entry get(String key) {
CacheHeader entry = mEntries.get(key);
if (entry == null) {
return null;
}
File file = getFileForKey(key);
try {
CountingInputStream cis = new CountingInputStream(
new BufferedInputStream(createInputStream(file)), file.length());
try {
CacheHeader entryOnDisk = CacheHeader.readHeader(cis);
if (!TextUtils.equals(key, entryOnDisk.key)) {
// 一個文件可能映射着兩個不一樣的 key,保存在不一樣的 Entry 中
removeEntry(key);
return null;
}
byte[] data = streamToBytes(cis, cis.bytesRemaining());
return entry.toCacheEntry(data);
} finally {
cis.close();
}
} catch (IOException e) {
remove(key);
return null;
}
}
複製代碼
咱們在上面說道,Volley 將響應的 data 放在磁盤中,將 CacheHeader 緩存在內存中,而 get() 方法其實就是這個過程的逆過程,先經過 key 從 mEntries 從取出 CacheHeader,若是爲 null,就直接返回 null,不然經過 key 來獲取磁盤中的 data,並經過 entry.toCacheEntry(data) 將 CacheHeader 和 data 拼接成完整的 Entry 而後進行返回。
看完了 DiskBaseCache 的具體實現,咱們最後看下 DiskBaseCache 在 Volley 中是怎麼使用的,這樣就能把 Volley 的緩存機制所有串聯起來了。
private static RequestQueue newRequestQueue(Context context, Network network) {
File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);
RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
queue.start();
return queue;
}
複製代碼
應該還記得 Volley 的基本使用方法吧,當時咱們第一步就是使用 Volley.newRequestQueue() 來建立一個 RequestQueue,這也是一切的起點。能夠看到咱們先經過 context.getCacheDir() 獲取緩存路徑,而後建立咱們緩存所需的目錄 cacheDir,這其實就是在 DiskBaseCache 中的 mRootDirectory,而後將其傳入 DiskBaseCache 只有一個參數的構造器中,建立了 DiskBaseCache 的實例,默認的內存緩存空間是 5M.
private static final int DEFAULT_DISK_USAGE_BYTES = 5 * 1024 * 1024;
public DiskBasedCache(File rootDirectory) {
this(rootDirectory, DEFAULT_DISK_USAGE_BYTES);
}
複製代碼
public class CacheDispatcher extends Thread {
@Override
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
mCache.initialize();
while (true) {
try {
processRequest();
} catch (InterruptedException e) {
}
}
}
}
複製代碼
initialize() 是在 CacheDispatcher 中的 run 方法進行調用的,CacheDispatcher 是處理緩存隊列中請求的線程。實例化 DiskBaseCache 以後,便在 while(true) 這個無線的循環當中,不斷地等請求的到來,而後執行請求。
public class NetworkDispatcher extends Thread {
private void processRequest() throws InterruptedException {
Request<?> request = mQueue.take();
try {
NetworkResponse networkResponse = mNetwork.performRequest(request);
Response<?> response = request.parseNetworkResponse(networkResponse);
if (request.shouldCache() && response.cacheEntry != null) {
mCache.put(request.getCacheKey(), response.cacheEntry);
request.addMarker("network-cache-written");
}
}
}
複製代碼
能夠看到 put() 方法是在 NetworkDispatcher 中進行調用的,NetworkDispatcher 是一個執行網絡請求的線程,從請求隊列中取出 Request,而後執行請求,若是 Request 是須要被緩存的(默認狀況下是必須被緩存的)並且 response 的 cacheEntry 不爲 null,就調用 DiskBaseCache 的 put() 方法將 Entry 進行緩存。
public class CacheDispatcher extends Thread {
@Override
public void run() {
mCache.initialize();
while (true) {
try {
processRequest();
} catch (InterruptedException e) {
}
}
}
private void processRequest() throws InterruptedException {
final Request<?> request = mCacheQueue.take();
// 調用 get() 方法獲取 Entry
Cache.Entry entry = mCache.get(request.getCacheKey());
if (entry.isExpired()) {
request.setCacheEntry(entry);
mNetworkQueue.put(request);
return;
}
Response<?> response = request.parseNetworkResponse(
new NetworkResponse(entry.data, entry.responseHeaders));
if (!entry.refreshNeeded()) {
mDelivery.postResponse(request, response);
}
}
複製代碼
咱們在上面說到 DiskBaseCache 的 initialize() 方法是在 CacheDispatcher 中的 run() 方法中調用,其實 get() 方法也是同樣的,在 while(true) 裏面無限循環,當有請求到來時,便先根據請求的 Url 拿出對應的緩存在內存中的 Entry,而後對 Entry 進行一些判斷和處理,最後將其構建成 Response 回調出去。
在調用 Volley.newRequestQueue() 方法獲取 RequestQueue 的時候,構建 DiskBaseCache 實例,在 CacheDispatcher 的 run() 方法中調用 DiskBaseCache 的 initialize() 方法初始化 DiskBaseCache,在 NetworkDispatcher 的 run() 方法中,在執行請求的時候,調用 DiskBaseCache 的 put() 方法將其緩存到內存中,而後在 CaheDispatcher 的 run() 方法中執行請求的時候調用 DiskBaseCache 的 get() 方法構建相應的 Response,最後將其分發出去。