若是你还不知道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 源码解析