更多关于安卓源码分析文章,请看:安卓源码分析
Volley源码分析系列:
1.聊下Volley源码(整体流程)
2.聊聊Volley源码(网络请求过程)
3.聊聊Volley源码(缓存流程)
上一下谈了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缓存机制还不是很熟悉,可能有说错或者遗漏额地方,希望各位指正。