首页 > 学院 > 开发设计 > 正文

聊聊Volley源码(缓存流程)

2019-11-07 22:50:30
字体:
来源:转载
供稿:网友

上一下谈了Volley网络请求流程聊聊Volley源码(网络请求过程),今天来谈下请求的缓存流程。

首先必须明确的是缓存的概念:缓存是“存贮数据(使用频繁的数据)的临时地方,因为取原始数据的代价太大了,可以取得快一些。”

在讲缓存流程之前,首先需要说明下Http的缓存机制,只有熟悉了Http的缓存机制,才可以理解Volley的缓存机制,因为Volley 构建了一套相对完整的符合 Http 语义的缓存机制。

首先看下Http换缓存相关首部的表:

简单概括缓存机制是这样的: 首次请求得到的响应头有Cache-Control或Expires(前者优先级高于后者,即同时出现以Cache-Control为准),则以后对相同链接再次请求: Cache-Control的max-age加响应Date首部时间或Expires对比当前时间点,当前时间小于则取本地缓存,否则发起请求。 如果发起请求,则将首次请求得到的响应头Last-Modified的值放入If-Modified-Since中,ETag的值放入If-None-Match进行请求, 服务器根据这两个首部,判断客户端的缓存是否过期,是则返回资源数据,否则返回304响,响应体为客户端本地的缓存数据。

说完了Http缓存机制,继续Volley源码。 一切又是要从RequestQueue的add说起: 不复制整个add方法代码了,直接上重点代码:

// Insert request into stage if there's already a request with the same cache key in flight. synchronized (mWaitingRequests) { String cacheKey = request.getCacheKey(); if (mWaitingRequests.containsKey(cacheKey)) { // There is already a request in flight. Queue up. Queue<Request<?>> stagedRequests = mWaitingRequests.get(cacheKey); if (stagedRequests == null) { stagedRequests = new LinkedList<Request<?>>(); } stagedRequests.add(request); mWaitingRequests.put(cacheKey, stagedRequests); if (VolleyLog.DEBUG) { VolleyLog.v("Request for cacheKey=%s is in flight, putting on hold.", cacheKey); } } else { // Insert 'null' queue for this cacheKey, indicating there is now a request in // flight. mWaitingRequests.put(cacheKey, null); mCacheQueue.add(request); } return request; }

当请求需要缓存时(默认需要缓存),就会执行以上代码。mWaitingRequests是存放重复的请求HashMap,如果发现当前的请求还在其中,则将请求添加入该HashMap中,如果当前请求不在该HashMap中,则将对应的cacheKey(就是请求的url)添加到mWaitingRequests中,然后将请求添加到缓存队列 CacheQueue中。(貌似这个上一篇已经讲过了,当做复习。。)

这里请求只是添加到缓存队列,并没有添加到请求队列中。所以下一步就是看 CacheDispatcher中如何处理CacheQueue中的请求。

@Override public void run() { if (DEBUG) VolleyLog.v("start new dispatcher"); PRocess.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); // Make a blocking call to initialize the cache. mCache.initialize(); while (true) { try { // Get a request from the cache triage queue, blocking until // at least one is available. final Request<?> request = mCacheQueue.take(); request.addMarker("cache-queue-take"); // If the request has been canceled, don't bother dispatching it. if (request.isCanceled()) { request.finish("cache-discard-canceled"); continue; } // Attempt to retrieve this item from cache. Cache.Entry entry = mCache.get(request.getCacheKey()); if (entry == null) { request.addMarker("cache-miss"); // Cache miss; send off to the network dispatcher. mNetworkQueue.put(request); continue; } // If it is completely expired, just send it to the network. if (entry.isExpired()) { request.addMarker("cache-hit-expired"); request.setCacheEntry(entry); mNetworkQueue.put(request); continue; } // We have a cache hit; parse its data for delivery back to the request. request.addMarker("cache-hit"); Response<?> response = request.parseNetworkResponse( new NetworkResponse(entry.data, entry.responseHeaders)); request.addMarker("cache-hit-parsed"); if (!entry.refreshNeeded()) { // Completely unexpired cache hit. Just deliver the response. mDelivery.postResponse(request, response); } else { // Soft-expired cache hit. We can deliver the cached response, // but we need to also send the request to the network for // refreshing. request.addMarker("cache-hit-refresh-needed"); request.setCacheEntry(entry); // Mark the response as intermediate. response.intermediate = true; // Post the intermediate response back to the user and have // the delivery then forward the request along to the network. 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; } } }

和网络请求分发类NetworkDispatcher中取请求的流程非常相似,在第一篇Volley源码分析文章中也有讲到,也是循环将请求从缓存队列中取出,执行相应的处理。

首先看第七行的:

mCache.initialize();

mCache就是进行缓存的核心类,请求都是通过该类缓存在指定的地方,这里是对其进行初始化工作。这里默认为DiskBasedCache,原理是将数据以流的形式写入到磁盘文件。具体文件位置可以指定,默认在Volley类的newRequestQueue方法创建RequestQueue的时候指定:

File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);

这里的DEFAULT_CACHE_DIR为“volley”,即在当前应用的Cache目录下创建了volley文件作为DiskBasedCache的缓存目录。

然后在循环中取出请求后,也是判断请求是否被取消,取消则将请求标记为取消并继续取下一个请求。

然后通过CacheKey判断下是否请求已经在缓存中,如果在的话则取出缓存的数据。

Cache.Entry entry = mCache.get(request.getCacheKey());

首先要说的是请求的缓存都是以Cache接口的内部类Entry 缓存起来的。

public static class Entry { /** The data returned from cache. */ public byte[] data; /** ETag for cache coherency. */ public String etag; /** Date of this response as reported by the server. */ public long serverDate; /** The last modified date for the requested object. */ public long lastModified; /** TTL for this record. */ public long ttl; /** Soft TTL for this record. */ public long softTtl; /** Immutable response headers as received from server; must be non-null. */ public Map<String, String> responseHeaders = Collections.emptyMap(); /** True if the entry is expired. */ public boolean isExpired() { return this.ttl < System.currentTimeMillis(); } /** True if a refresh is needed from the original data source. */ public boolean refreshNeeded() { return this.softTtl < System.currentTimeMillis(); } }

Entry 是一个很简单的实体类,存储的是和请求判断是否过期相关的属性,那这里的缓存过程是在哪里进行的呢?这很容易猜到,在网络请求成功后。上一篇在网络请求成功后,跳过了缓存的过程,现在就来谈下。

其实在NetworkDispatcher请求成功后,会执行以下语句:

// Parse the response here on the worker thread. Response<?> response = request.parseNetworkResponse(networkResponse);

这是将得到的NetWorkResponse对象解析为具体对象的Response,在这里,具体的Request对象会将其中的数据转化为Cache的Entry对象,比如StringRequest:

@Override protected Response<String> parseNetworkResponse(NetworkResponse response) { String parsed; try { parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers)); } catch (UnsupportedEncodingException e) { parsed = new String(response.data); } return Response.success(parsed, HttpHeaderParser.parseCacheHeaders(response)); }

最后一句调用了HttpHeaderParser的parseCacheHeaders,进去看:

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 = null; String headerValue; //提取出响应头Date,若存在则保存在serverDate headerValue = headers.get("Date"); if (headerValue != null) { serverDate = parseDateAsEpoch(headerValue); } //提取出响应头Cache-Control headerValue = headers.get("Cache-Control"); if (headerValue != null) { hasCacheControl = true; //取出Cache-Control头相应的值 String[] tokens = headerValue.split(","); for (int i = 0; i < tokens.length; i++) { String token = tokens[i].trim(); //不取缓存数据或不进行缓存 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.startsWith("stale-while-revalidate=")) { try { staleWhileRevalidate = Long.parseLong(token.substring(23)); } catch (Exception e) { } } else if (token.equals("must-revalidate") || token.equals("proxy-revalidate")) { mustRevalidate = true; } } } //提取出响应头Expires,若存在则保存在serverExpires headerValue = headers.get("Expires"); if (headerValue != null) { serverExpires = parseDateAsEpoch(headerValue); } //提取出响应头Last-Modified,若存在则保存在lastModified headerValue = headers.get("Last-Modified"); if (headerValue != null) { lastModified = parseDateAsEpoch(headerValue); } //提取出响应头ETag,若存在则保存在serverEtag serverEtag = headers.get("ETag"); // Cache-Control takes precedence over an Expires header, even if both exist and Expires // is more restrictive. //有Cache-Control响应头的情况下 if (hasCacheControl) { //响应数据缓存最迟有效时间 softExpire = now + maxAge * 1000; //需要进行新鲜度验证最迟时间 finalExpire = mustRevalidate ? softExpire : softExpire + staleWhileRevalidate * 1000; //如果没有Cache-Control响应头,则以Expires的时间为过期时间 } else if (serverDate > 0 && serverExpires >= serverDate) { // Default semantic for Expire header in HTTP specification is softExpire. softExpire = now + (serverExpires - serverDate); finalExpire = softExpire; } //将响应体和响应头等相关数据保存在Entry中 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; }

结合注释和前面说的Http缓存机制应该就可以理解。然后在将结果传递到客户端之前执行:

if (request.shouldCache() && response.cacheEntry != null) { mCache.put(request.getCacheKey(), response.cacheEntry); request.addMarker("network-cache-written"); }

这样就把生成的Cache的Entry以request的cacheKey为key缓存起来了。

回到CacheDispather的run方法,如果获取到的entry为null,说明当前请求没有被缓存过,就直接将其添加到请求队列中,走上一篇讲到的网络请求流程去验证响应数据新鲜度,然后取缓存队列下一个请求。

如果获取到的Entry对象为不为null,则说明当前请求有缓存结果,所以在30行判断entry.isExpired(),即缓存是否过期(根据前面在HttpHeaderParser 解析的响应头算出来的属性ttl与当前时间的比较),如果过期,执行:

request.setCacheEntry(entry);

将该Entry传递给request,然后将请求重新添加到请求队列中,重新请求。

如果没有过期,再判断是否需要验证新鲜度entry.refreshNeeded()(softTtl与当前时间的比较),不用验证新鲜度则直接将缓存的数据传递到客户端线程,需要刷新则还是将将该Entry传递给request,然后请求添加到请求队列去验证响应数据新鲜度。

执行请求的BasicNetwork的 performRequest方法中,调用了 addCacheHeaders方法:

private void addCacheHeaders(Map<String, String> headers, Cache.Entry entry) { // If there's no cache entry, we're done. if (entry == null) { return; } if (entry.etag != null) { headers.put("If-None-Match", entry.etag); } if (entry.lastModified > 0) { Date refTime = new Date(entry.lastModified); headers.put("If-Modified-Since", DateUtils.formatDate(refTime)); } }

就是在请求存在Entry对象的情况下(即请求是由缓存中取出),添加If-None-Match请求头,值为原来响应头Etag的值,以及If-Modified-Since请求头,值为原来响应头Last-Modified的值。

通过这两个请求头,就像前面讲Http缓存机制一样,服务端和资源的更新时间进行比较,如果发现资源的Etag和Last-Modified一致,则认定缓存有效,则返回响应码为304的响应,并且客户端会将请求携带的Entry中的数据(响应实体)和响应头作为新的响应返回:

responseHeaders = convertHeaders(httpResponse.getAllHeaders()); // Handle cache validation. if (statusCode == HttpStatus.SC_NOT_MODIFIED) { Entry entry = request.getCacheEntry(); if (entry == null) { return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, null, responseHeaders, true, SystemClock.elapsedRealtime() - requestStart); } // A HTTP 304 response does not have all header fields. We // have to use the header fields from the cache entry plus // the new ones from the response. // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5 entry.responseHeaders.putAll(responseHeaders); return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, entry.data, entry.responseHeaders, true, SystemClock.elapsedRealtime() - requestStart); }

这里就是取出新响应的状态码后执行的代码,HttpStatus.SC_NOT_MODIFIED就是304,所以在拿到304响应状态码后,利用原来缓存的响应实体和头构建一个NetworkResponse返回。

剩下的工作就和网络请求成功后的流程一样了。 介于个人对于Http缓存机制还不是很熟悉,可能有说错或者遗漏额地方,希望各位指正。


发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表