若是你還不知道volley有磁盤緩存的話,請看一下個人另外一篇博客請注意,Volley已默認使用磁盤緩存html
它由兩部分組成,一部分是頭部,一部分是內容;先得從它的內部靜態類CacheHeader(緩存的頭部信息)講起,先看它的內部結構:java
static class CacheHeader { /** 緩存文件的大小 */ public long size; /** 緩存文件的惟一標識 */ public String key; /** 這個是與與http請求緩存相關的標籤 */ public String etag; /** 服務器的返回來數據的時間 */ public long serverDate; /** TTL 緩存過時時間. */ public long ttl; /** Soft TTL 緩存新鮮度時間. */ public long softTtl; /** 服務器還回來的頭部信息. */ public Map<String, String> responseHeaders; } //能夠看到,頭部類裏包含的都是一些基本信息。再來看一下內容部分,父類Cache裏面的的Entry: public static class Entry { /** 服務端返回數據的主要內容. */ public byte[] data; public String etag; public long serverDate; public long ttl; public long softTtl; }
能夠看到,Entry裏面和CacheHeader裏有四個參數是同樣的,只是Entry裏多了data[],data[]就是用來保存主要數據 的。看到這你能夠有點迷糊,Entry和CacheHeader裏爲何要有四個參數同樣,先簡單說一下緣由:volley框架裏都用到接口編程,因此實 際代碼中除了初始化,你只看到cache,而DiskBasedCache是看不到的,因此必須在Entry裏先把那些緩存須要用到的參數保留起來,而後 具體實現和封裝放在DiskBasedCache裏。android
初始化編程
DiskBasedCache的初始化時在RequestQueue新建時就發生的,能夠看Volley.newRequestQueue()的源碼:json
public static RequestQueue newRequestQueue(Context context, HttpStack stack, int maxDiskCacheBytes) { File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR); .... //maxDiskCacheBytes爲緩存的最大容量,不傳就默認爲5M if (maxDiskCacheBytes <= -1) { // No maximum size specified queue = new RequestQueue(new DiskBasedCache(cacheDir), network); } else { // Disk cache size specified queue = new RequestQueue(new DiskBasedCache(cacheDir, maxDiskCacheBytes), network); } queue.start(); return queue; }
能夠看到,磁盤緩存的路徑爲:context.getCacheDir(),若是maxDiskCacheBytes有傳入,就以傳入的爲準,若是爲空:緩存
/** 默認的磁盤存放的最大byte */ private static final int DEFAULT_DISK_USAGE_BYTES = 5 * 1024 * 1024; ... public DiskBasedCache(File rootDirectory) { this(rootDirectory, DEFAULT_DISK_USAGE_BYTES); }
磁盤默認爲5M。因此若是你想設置最大的磁盤緩存值,那麼就不能直接向下面那樣這樣初始化了:服務器
queue = Volley.newRequestQueue(context);
而是須要這樣:markdown
queue = Volley.newRequestQueue(context, 10 * 1024 * 1024);
存放緩存數據網絡
第一次緩存的數據是從哪來的呢,固然是從網上來,看NetWorkDispatcher的run方法裏:框架
@Override public void run() { while (true) { ... try { .... 請求解析http的返回信息 .... if (request.shouldCache() && response.cacheEntry != null) { mCache.put(request.getCacheKey(), response.cacheEntry); request.addMarker("network-cache-written"); } .... } }
其中request.getCacheKey()默認爲請求的url,response.cacheEntry是Cache.Entry,裏面已存 放好解析完的httpResponse數據,request.shouldCache()默認是須要緩存,若是不須要可調用 request.setShouldCache(false)來去掉緩存功能。
咱們把請求和處理的http的返回略過,留下幾行關鍵代碼,若是這個請求須要緩存(默認須要)和緩存信息不爲空,那麼就保存緩存信息。接下來看,DiskBaseCache是怎麼保存緩存的:
/** * 把緩存數據Entry寫進磁盤裏 */ @Override public synchronized void put(String key, Entry entry) { //判斷是否有足夠的緩存空間來緩存新的數據 pruneIfNeeded(entry.data.length); File file = getFileForKey(key); try { FileOutputStream fos = new FileOutputStream(file); //用enry裏面的數據,再封裝成一個CacheHeader CacheHeader e = new CacheHeader(key, entry); //先寫頭部緩存信息 boolean success = e.writeHeader(fos); if (!success) { fos.close(); VolleyLog.d("Failed to write header for %s", file.getAbsolutePath()); throw new IOException(); } //成功後再寫緩存內容 fos.write(entry.data); fos.close(); //把頭部信息先暫時保存在一個容器裏 putEntry(key, e); return; } catch (IOException e) { } boolean deleted = file.delete(); if (!deleted) { VolleyLog.d("Could not clean up file %s", file.getAbsolutePath()); } }
能夠看到,每次寫入緩存以前,都先調用pruneIfNeeded()檢查對象的大小,當緩衝區空間足夠新對象的加入時就直接添加進來,不然會刪除 部分對象,一直到新對象添加進來後還會有10%的空間剩餘時爲止,文件引用以LinkHashMap保存。添加時,首先以URL爲key,通過個文本轉換 後,以轉換後的文本爲名稱,獲取一個file對象。首先向這個對象寫入緩存的頭文件,而後是真正有用的網絡返回數據。最後是當前內存佔有量數值的更新,這 裏須要注意的是真實數據被寫入磁盤文件後,在內存中維護的應用,存的只是數據的相關屬性。
咱們知道隊列建立後就會有一個緩存線程在後臺一直運行等待着緩存請求進來,但在等待線程前,會先調用mCache.initialize(),把緩存數據的頭部信息放進一個Map類型mEntries裏,這樣之後要用到就先用mEntries判斷,速度更快。
若是請求進來即調用Cache.Entry entry = mCache.get(request.getCacheKey())
,那咱們就看DiskBaseCache。get方法裏作了什麼:
@Override public synchronized Entry get(String key) { CacheHeader entry = mEntries.get(key); // 若是entry不爲空,就直接返回 if (entry == null) { return null; } File file = getFileForKey(key); CountingInputStream cis = null; try { cis = new CountingInputStream(new FileInputStream(file)); CacheHeader.readHeader(cis); // eat header byte[] data = streamToBytes(cis, (int) (file.length() - cis.bytesRead)); return entry.toCacheEntry(data); } catch (IOException e) { VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString()); remove(key); return null; } finally { if (cis != null) { try { cis.close(); } catch (IOException ioe) { return null; } } } }
從方法裏能夠看到,先從文件裏得到字節數輸入流,從中減去頭部文件的字節數,最後把真正內容的data[]數據拿到再組裝成一個Cache.Entry返回。不得不說,Volley這真是精打細算啊。
從上面的分析可見,cache在作一些基礎判斷時都會先用到緩存的頭部數據,若是肯定頭部信息沒問題了,再真正讀寫內容,緣由是頭部數據比較小,放在內存中也不佔地方,但處理速度會快不少。而真正的數據內容,可能會比較大,處理的開銷也大,只在真正須要的地方讀寫。
http的304狀態碼的含義是:
若是服務器端的資源沒有變化,則自動返回 HTTP 304 (Not Changed.)狀態碼,內容爲空,這樣就節省了傳輸數據量。當服務器端代碼發生改變或者重啓服務器時,則從新發出資源,返回和第一次請求時相似。從而 保證不向客戶端重複發出資源,也保證當服務器有變化時,客戶端可以獲得最新的資源。
完整的過程以下:
介紹完304,咱們接下來來看看volley是怎麼運用304來重用緩存的。
首先咱們來看一下對於response.header的處理,在每個request裏,都必須繼承 parseNetworkResponse(NetworkResponse response)方法,而後在裏面用 HttpHeaderParser.parseCacheHeaders()解析類來解析頭部數據,具體以下:
public static Cache.Entry parseCacheHeaders(NetworkResponse response) { long now = System.currentTimeMillis(); Map<String, String> headers = response.headers; long serverDate = 0; long serverExpires = 0; long softExpire = 0; long maxAge = 0; boolean hasCacheControl = false; String serverEtag = null; String headerValue; headerValue = headers.get("Date"); if (headerValue != null) { serverDate = parseDateAsEpoch(headerValue); } headerValue = headers.get("Cache-Control"); if (headerValue != null) { hasCacheControl = true; String[] tokens = headerValue.split(","); for (int i = 0; i < tokens.length; i++) { String token = tokens[i].trim(); //若是Cache-Control裏爲no-cache和no-store則表示不須要緩存,返回null if (token.equals("no-cache") || token.equals("no-store")) { return null; } else if (token.startsWith("max-age=")) { try { maxAge = Long.parseLong(token.substring(8)); } catch (Exception e) { } } else if (token.equals("must-revalidate") || token.equals("proxy-revalidate")) { maxAge = 0; } } } headerValue = headers.get("Expires"); if (headerValue != null) { serverExpires = parseDateAsEpoch(headerValue); } serverEtag = headers.get("ETag"); // Cache-Control takes precedence over an Expires header, even if both exist and Expires // is more restrictive. if (hasCacheControl) { softExpire = now + maxAge * 1000; } else if (serverDate > 0 && serverExpires >= serverDate) { // Default semantic for Expire header in HTTP specification is softExpire. softExpire = now + (serverExpires - serverDate); } Cache.Entry entry = new Cache.Entry(); entry.data = response.data; entry.etag = serverEtag; entry.softTtl = softExpire; entry.ttl = entry.softTtl; entry.serverDate = serverDate; entry.responseHeaders = headers; return entry; }
從上面代碼能夠看出緩存頭部是根據 Cache-Control 和 Expires 首部,計算出緩存的過時時間(ttl),和緩存的新鮮度時間(softTtl,默認softTtl和ttl相同),若是有Cache-Control標籤 以它爲準,沒有就以Expires標籤裏的內容爲準。
須要注意的是:Volley沒有處理Last-Modify首部,而是處理存儲了Date首部,並在後續的新鮮度驗證時,使用Date來構建If-Modified-Since。 這與 Http 1.1 的語義有些違背。
在使用緩存數據前,Volley會先對驗證緩存數據是否過時,是否須要更新等屬性,而後一一處理,代碼在CacheDispatcher的run方法裏:
@Override public void run() { if (DEBUG) VolleyLog.v("start new dispatcher"); Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); // 初始化緩存,裏面會先把磁盤緩存裏的頭部數據緩存進內存裏,增長處理速度 mCache.initialize(); while (true) { try { // 阻塞線程直到有請求加入,纔開始運行 final Request request = mCacheQueue.take(); request.addMarker("cache-queue-take"); //請求是否取消 if (request.isCanceled()) { request.finish("cache-discard-canceled"); continue; } // 獲得緩存數據entry Cache.Entry entry = mCache.get(request.getCacheKey()); //若是緩存不存在,就把請求交給網絡隊列取處理 if (entry == null) { request.addMarker("cache-miss"); mNetworkQueue.put(request); continue; } // 若是請求過時,也須要到網絡從新獲取數據 if (entry.isExpired()) { request.addMarker("cache-hit-expired"); request.setCacheEntry(entry); mNetworkQueue.put(request); continue; } // 到這裏就代表緩存數據是可用的,解析緩存 request.addMarker("cache-hit"); Response<?> response = request.parseNetworkResponse( new NetworkResponse(entry.data, entry.responseHeaders)); request.addMarker("cache-hit-parsed"); //驗證緩存的新鮮度 if (!entry.refreshNeeded()) { //新鮮的 mDelivery.postResponse(request, response); } else { // 不新鮮,雖然把緩存數據分發出去,但仍是須要到網絡上驗證緩存是否須要更新 request.addMarker("cache-hit-refresh-needed"); //請求帶上緩存屬性 request.setCacheEntry(entry); response.intermediate = true; // 分發完緩存數據後,將請求加入網絡請求隊列,判斷是否須要更新緩存數據 mDelivery.postResponse(request, response, new Runnable() { @Override public void run() { try { mNetworkQueue.put(request); } catch (InterruptedException e) { // Not much we can do about this. } } }); } } catch (InterruptedException e) { // We may have been interrupted because it was time to quit. if (mQuit) { return; } continue; } } }
上面代碼都已經加了註釋,相信不難理解,那咱們繼續看,網絡請求是怎麼判斷是否須要更新緩存的,在BasicNetwork.performRequest()裏:
@Override public NetworkResponse performRequest(Request<?> request) throws VolleyError { .... while (true) { ... try { Map<String, String> headers = new HashMap<String, String>(); //若是請求有帶屬性,就將etag和If-Modified-Since屬性加上 addCacheHeaders(headers, request.getCacheEntry()); httpResponse = mHttpStack.performRequest(request, headers); StatusLine statusLine = httpResponse.getStatusLine(); int statusCode = statusLine.getStatusCode(); responseHeaders = convertHeaders(httpResponse.getAllHeaders()); //若是304就直接用緩存數據返回 if (statusCode == HttpStatus.SC_NOT_MODIFIED) { return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, request.getCacheEntry().data, responseHeaders, true); } ..... return new NetworkResponse(statusCode, responseContents, responseHeaders, false); } catch (SocketTimeoutException e) { ... } } }
從上面的註釋能夠看到,若是是返回304就直接用緩存數據返回。那來看NetworkDispatcher的run()裏:
public void run() { ... NetworkResponse networkResponse = mNetwork.performRequest(request); request.addMarker("network-http-complete"); // 若是是304而且已經將緩存分發出去裏,就直接結束這個請求 if (networkResponse.notModified && request.hasHadResponseDelivered()) { request.finish("not-modified"); continue; } ... } }
如今流程比較清晰了,在有緩存的狀況下,若是已通過期,可是返回304,就複用緩存。若是不新鮮了,就先將緩存分發出去,而後再進行網絡請求,看是否須要更新緩存。
不過眼尖的讀者必定有個疑惑,在解析頭部數據時,默認不是新鮮度和過時事件是同樣的嗎?那新鮮度不是必定運行不到嗎?確實是這樣,我也有這個疑惑, 網上也找不到確切的資料來解釋這一點。不過按照正常的邏輯,新鮮度時間必定比過時時間短,這樣咱們就能夠根據實際須要更改Volley的源碼。例如,咱們 能夠直接把新鮮度的驗證時間設爲3分鐘,而過時時間設爲一天,代碼以下:
public static Cache.Entry parseIgnoreCacheHeaders(NetworkResponse response) { long now = System.currentTimeMillis(); Map<String, String> headers = response.headers; long serverDate = 0; String serverEtag = null; String headerValue; headerValue = headers.get("Date"); if (headerValue != null) { serverDate = HttpHeaderParser.parseDateAsEpoch(headerValue); } serverEtag = headers.get("ETag"); final long cacheHitButRefreshed = 3 * 60 * 1000; final long cacheExpired = 24 * 60 * 60 * 1000; final long softExpire = now + cacheHitButRefreshed; final long ttl = now + cacheExpired; Cache.Entry entry = new Cache.Entry(); entry.data = response.data; entry.etag = serverEtag; entry.softTtl = softExpire; entry.ttl = ttl; entry.serverDate = serverDate; entry.responseHeaders = headers; return entry; }
而後使用的時候:
public class MyRequest extends com.android.volley.Request<MyResponse> { ... @Override protected Response<MyResponse> parseNetworkResponse(NetworkResponse response) { String jsonString = new String(response.data); MyResponse MyResponse = gson.fromJson(jsonString, MyResponse.class); return Response.success(MyResponse, HttpHeaderParser.parseIgnoreCacheHeaders(response)); } }
這樣的話,在3分鐘後就不新鮮,24小時後就會過時。
咱們使用ImageLoader時會傳入一個ImageCache,它是個接口,裏面定義了兩個方法:
public interface ImageCache { public Bitmap getBitmap(String url); public void putBitmap(String url, Bitmap bitmap); }
那他們是何時使用的呢,能夠從開始請求數據ImageLoader.get()方法看起:
public ImageContainer get(String requestUrl, ImageListener imageListener, int maxWidth, int maxHeight) { //請求只能在主線程裏,否則會報錯 throwIfNotOnMainThread(); //用url和寬高組成key final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight); //從內存緩存裏獲取數據 Bitmap cachedBitmap = mCache.getBitmap(cacheKey); if (cachedBitmap != null) { // 若是內存不爲空,直接返回圖片信息 ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null); imageListener.onResponse(container, true); return container; } ... // 若是爲空,就正常請求網絡數據,下面用的是ImageRequest取請求網絡數據 Request<?> newRequest = new ImageRequest(requestUrl, new Listener<Bitmap>() { @Override public void onResponse(Bitmap response) { //請求成功後,在這個方法裏,把圖片放進內存緩存中 onGetImageSuccess(cacheKey, response); } }, maxWidth, maxHeight, Config.RGB_565, new ErrorListener() { @Override public void onErrorResponse(VolleyError error) { onGetImageError(cacheKey, error); } }); ... } private void onGetImageSuccess(String cacheKey, Bitmap response) { //把圖片放進內存裏 mCache.putBitmap(cacheKey, response); ... }
從上面的代碼註釋中已經能比較清晰的看出,每次調用ImageLoader.get()方法,會先從內存緩存裏先看有沒有數據,有就直接返回,沒有 就走正常的網絡流程,先查看磁盤緩存,不存在或過時再去請求網絡。圖片比普通數據多一層緩存的緣由也很簡單,由於圖片較大,讀取和網絡成本都大,能用緩存 就用緩存,能省一點是一點。
下面來看看具體的流程圖
以上就是Volley框架所使用到的全部緩存機制,若有遺漏請留言指出,多謝閱讀。
參考連接:
Volley網絡請求源碼解析——擊潰6大疑慮
Last-Modified和If-Modified-Since
Volley 源碼解析