序
本文是承接Volley源码解析之---一次完整的StringRequest请求(1)的第二篇。上一篇文章,主要介绍了NetworkDispatcher
以及BasicNetwork
等,如果没有读过上篇文章的建议先读上一篇文章再读这个,才能更好地连贯起来。接下来我们将继续讲解CacheDispatcher
和RequestQueue.add
。看看他们分别都干了些什么!
CacheDispatcher
在上一篇文章中我们提到,RequestQueue
的Start
方法中开启了CacheDispatcher
。在我看来,它是用来协助缓存请求的。先来看看它的构造函数:
/**
* Creates a new cache triage dispatcher thread. You must call {@link #start()}
* in order to begin processing.
*
* @param cacheQueue Queue of incoming requests for triage
* @param networkQueue Queue to post requests that require network to
* @param cache Cache interface to use for resolution
* @param delivery Delivery interface to use for posting responses
*/
public CacheDispatcher(
BlockingQueue<Request<?>> cacheQueue, BlockingQueue<Request<?>> networkQueue,
Cache cache, ResponseDelivery delivery) {
mCacheQueue = cacheQueue;
mNetworkQueue = networkQueue;
mCache = cache;
mDelivery = delivery;
}
可以看到他其实就是比NetworkDispatcher
多了一个mCacheQueue
的阻塞队列,其他三个参数的意义和NetworkDispatcher
的参数含义是一样的。这里我就不重复啰嗦了。
接着我们看看它的run
方法。
@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()) {
//判断是否已经被取消,如果被取消了那么就直接finish掉本次请求,进行下一次请求
request.finish("cache-discard-canceled");
continue;
}
// Attempt to retrieve this item from cache. 根据缓存的key从缓存里面获取缓存的数据
Cache.Entry entry = mCache.get(request.getCacheKey());
if (entry == null) {
//如果缓存中没有找到key对应的数据,那么就将本次请求放入mNetworkQueue
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()) {
//判断缓存是否过期,如果过期了,同样将请求添加到mNetworkQueue但是同时给请求设置CacheEntry
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<?> response = request.parseNetworkResponse(
new NetworkResponse(entry.data, entry.responseHeaders));
request.addMarker("cache-hit-parsed");
//判断缓存是否需要刷新。如果不需要刷新,直接通过mDelivery将请求结果回调给请求调用者
if (!entry.refreshNeeded()) {
// Completely unexpired cache hit. Just deliver the response.
mDelivery.postResponse(request, response);
} else {
//如果需要刷新,则先直接将请求的结果回调给请求调用者,但是同时将请求加入mNetworkQueue进行网络请求
// 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;
}
}
}
}
同样的上面我在必要的地方都添加了注释,然后我们开始讲解一个一个的重点。
- 第一个
mCache.initialize();
从方法名可以知道是用来初始化什么东西的,那么这个方法都初始化了什么呢。让我们跳到方法里面看看,注默认的Cache是DiskBasedCache
.
/**
* 扫描当前所有的缓存文件,初始化DiskBasedCache,如果根目录不存在就创建根目录
* Initializes the DiskBasedCache by scanning for all files currently in the
* specified root directory. Creates the root directory if necessary.
*/
@Override
public synchronized void initialize() {
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;
}
for (File file : files) {
BufferedInputStream fis = null;
try {
fis = new BufferedInputStream(new FileInputStream(file));
CacheHeader entry = CacheHeader.readHeader(fis);
entry.size = file.length();
putEntry(entry.key, entry);
} catch (IOException e) {
if (file != null) {
file.delete();
}
} finally {
try {
if (fis != null) {
fis.close();
}
} catch (IOException ignored) { }
}
}
}
从上面的代码我们可以看到,initialize
就是通过遍历缓存目录中的文件,将缓存的请求头读取到内存中即mEntries
变量中,以供之后查询缓存使用。那么CacheHeader
都包括了那些字段呢,一起来看看CacheHeader.readHeader(fis)
方法。
/**
* Reads the header off of an InputStream and returns a CacheHeader object.
* @param is The InputStream to read from.
* @throws IOException
*/
public static CacheHeader readHeader(InputStream is) throws IOException {
CacheHeader entry = new CacheHeader();
int magic = readInt(is);
if (magic != CACHE_MAGIC) {
// don't bother deleting, it'll get pruned eventually
throw new IOException();
}
//缓存标示
entry.key = readString(is);
//资源的唯一标示
entry.etag = readString(is);
if (entry.etag.equals("")) {
entry.etag = null;
}
//
entry.serverDate = readLong(is);
//上一次修改的时间
entry.lastModified = readLong(is);
//硬过期时间,缓存无效
entry.ttl = readLong(is);
//软过期时间,虽然过期了,但是缓存还能使用
entry.softTtl = readLong(is);
//响应头
entry.responseHeaders = readStringStringMap(is);
return entry;
}
可以看到CacheHeader
除了保存了响应头之外,还保存了ttl
时间以及softTtl
时间,这两个时间都是和缓存过期期限有关系的,等会我们会更详细的解释,先记住有这么一个东西,lastModified
是上一次资源的时间,我们可以利用这个来判断服务器上的资源是否真的改变了。Etag
是资源的唯一标示,也可以用来判断资源是否过期。关于Etag
与lastModified
在什么情况下用来判断资源是否过期以及如何判断我们在上面一篇文章中已经有详细的说明,没有看的同学找到这篇文章看一看哦,当然如果你感兴趣的话。想要知道缓存文件是如何存储缓存数据的可以找到目录里的缓存文件看看。
就这样完成了初始化,那么继续,跟NetworkDispatcher
一样首先从阻塞队列里面中取出一个请求,不过这个是``mCacheQueue里面取出
Request,不是从
mNetworkQueue里面取,因为这是缓存请求。同样的由于是个阻塞队列,所以如果没有请求 那么就阻塞等待。跟
NetworkDispatcher`一样在处理请求之前,先判断一下请求是否被取消,如果已经被取消了,那么就finish掉整个请求,进行下一次请求。代码如下:
// If the request has been canceled, don't bother dispatching it.
if (request.isCanceled()) {
//判断是否已经被取消,如果被取消了那么就直接finish掉本次请求,进行下一次请求
request.finish("cache-discard-canceled");
continue;
}
当获取了请求之后,根据请求的CacheKey从缓存中取数据,如果缓存命中,则使用缓存的数据,如果命中缓存失败,则将请求添加进mNetworkQueue
进行网络请求。
// Attempt to retrieve this item from cache. 根据缓存的key从缓存里面获取缓存的数据
Cache.Entry entry = mCache.get(request.getCacheKey());
if (entry == null) {
//如果缓存中没有找到key对应的数据,那么就将本次请求放入mNetworkQueue
request.addMarker("cache-miss");
// Cache miss; send off to the network dispatcher.
mNetworkQueue.put(request);
continue;
}
缓存命中(即有该请求的缓存数据),则判断缓存是否过期 entry.isExpired()
,如果过期了那么就将请求加入mNetworkQueue
进行网络请求。首先让我们来看看,entry.isExpired()
这个方法。
/** True if the entry is expired. */
boolean isExpired() {
return this.ttl < System.currentTimeMillis();
}
原来这个方法就是判断一下,ttl小于当前时间,如果小于这说明缓存过期啦,应该重新请求新的数据了。那么ttl是哪里来的,接下来我们还会看到softTtl
,那么它又是什么决定的,接下来我们一起看一下。 还记得我们这个方法Response<JSONObject> parseNetworkResponse(NetworkResponse response)
吗,这是我们将NetworkResponse
转换成Response
的方法。因为我们是讲解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));
}
注意啦。在return的时候调用了HttpHeaderParser.parseCacheHeaders(response)
函数。正如其名这个就是将Response
的转成CacheHeader
。接下来一起来看看这个方法:
/**
* Extracts a {@link com.android.volley.Cache.Entry} from a {@link NetworkResponse}.
*
* @param response The network response to parse headers from
* @return a cache entry for the given response, or null if the response is not cacheable.
*/
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;
//获取服务器时间
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();
if (token.equals("no-cache") || token.equals("no-store")) {
//如果`no-cache`或者`no-store`都是控制不使用缓存直接向服务器请求,都表示则直接return null
return null;
} else if (token.startsWith("max-age=")) {
//表示缓存在xxx秒之后过期。
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;
}
}
}
//获取超期时间,不过这个是返回的服务器上面的时间为基准的,
// 所以如果客户端和服务器端时间相差很大,那么就会很不标准,
// 所以后来在Cache-Control里面添加max-age来控制,max-age优先级更高
headerValue = headers.get("Expires");
if (headerValue != null) {
serverExpires = parseDateAsEpoch(headerValue);
}
//资源上一次修改的时间
headerValue = headers.get("Last-Modified");
if (headerValue != null) {
lastModified = parseDateAsEpoch(headerValue);
}
//资源标示
serverEtag = headers.get("ETag");
// Cache-Control takes precedence over an Expires header, even if both exist and Expires
// is more restrictive.
//Cache-Control优先于Expires Header,所以先判断是否存在CacheControl 并且没有no-cache或no-store字段
if (hasCacheControl) {
//软过期时间(即虽然缓存过期了但是仍然可以使用缓存的时间范围),软超期时间等于现在的时间 + max-age * 1000
softExpire = now + maxAge * 1000;
//如果mustRevalidate存在,那么这个时候真正超期时间就等于软过期时间
//不存在的话,真正超期时间 = 软过期时间 + staleWhileRevalidate * 1000
finalExpire = mustRevalidate
? softExpire
: softExpire + staleWhileRevalidate * 1000;
} else if (serverDate > 0 && serverExpires >= serverDate) {
//Expire头部在HTTP协议中就是软超期时间,所以这个时候真正超期时间 == 软超期时间
// Default semantic for Expire header in HTTP specification is softExpire.
softExpire = now + (serverExpires - serverDate);
finalExpire = softExpire;
}
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;
}
从上面这个方法我们可以看到缓存中每个字段是如何计算得到的,尤其是软超期时间和真正超期的时间。我总结一下:
- 当Response中有Cache-Control并且没有no-cache以及no-store字段时,软过期时间等于当前时间+max-age * 1000 .如果存在must-revalidate或者proxy-revalidate时,则真正过期时间等于软过期时间 + stale-while-revalidate
- 如果CacheControl 不存在,则软过期时间 == 真正过期时间 == softExpire = now + (serverExpires - serverDate)
- 不过值得注意的是,Expire的日期是根据服务器的时间来定的,如果服务器和客户端的时间相差很大的话那么时间就不一致了。
而且我们可以知道ttl是最终过期时间,softTtl是软过期时间。所以entry.isExpired()
为true
则说明缓存过期了,则需要重新请求网络,所以直接添加到mNetworkQueue
里面进行网络请求。有些同学可能觉得奇怪既然都要重新请求了为什么还要把缓存中的entry添加到reqeust
这个对象呢,执行request.setCacheEntry(entry);
呢,在上一篇我们提到过虽然缓存过期了,但是并不代表服务器上的资源真的改变了,所以这个时候将上一次的LastModified
以及etag
传递过去,可以用于服务器验证资源是否过期的校验。接下来,当缓存没有真正过期,则从缓存中拿出上一次响应的数据,然后转换成Response
对象。但是现在还不能吧结果返回给请求调用者,还需要判断一下,接着看entry.refreshNeeded();
,从方法上来看,是判断是否需要更新,具体看看是如何判断的
/** True if a refresh is needed from the original data source. */
boolean refreshNeeded() {
return this.softTtl < System.currentTimeMillis();
}
可以看到这个方法是根据软过期时间(softTtl)来判断是否需要刷新,上面我们提到过,如果超过了软过期时间,虽然缓存还是可以用的,但是需要同时请求服务器获取新的数据,所以接下来就是根据是否需要刷新做不同的处理,如果不需要刷新那么就直接将Response
回调给请求调用者,即StringRequest
的Listener
里面。同样的 request.setCacheEntry(entry)
便于服务器验证,减少不必要的数据请求。
至此,我们把CacheDispatcher
的run
方法给理清楚了。
接下来,我们还剩下一个分析点没有分析了。那就是RequestQueue.add
对于这个方法,我自己一开始也有几个疑问:
- add之后做了什么
- request添加进去之后是不是通过
CatchDispatcher
来处理的。
直接看代码,才能找到答案,所以接下来我们一起来看看RequestQueue.add
都做了什么吧。
RequestQueue
首先贴上add
方法的源码:
/**
* Adds a Request to the dispatch queue.
* @param request The request to service
* @return The passed-in request
*/
public <T> Request<T> add(Request<T> request) {
// Tag the request as belonging to this queue and add it to the set of current requests.
//设置这个请求属于这个ReqeustQueue,并将这个请求添加到这个请求队列的当前请求队列中
//mCurrentRequests 是存放当前所有的请求的一个集合
request.setRequestQueue(this);
synchronized (mCurrentRequests) {
mCurrentRequests.add(request);
}
// Process requests in the order they are added.
//给request设置序号
request.setSequence(getSequenceNumber());
request.addMarker("add-to-queue");
// If the request is uncacheable, skip the cache queue and go straight to the network.
//判断是否可以缓存,如果不可以,就直接将请求添加进mNetworkQueue进行网络请求,默认都是true,当然你可以设成false
if (!request.shouldCache()) {
mNetworkQueue.add(request);
return request;
}
// Insert request into stage if there's already a request with the same cache key in flight.
//将请求插入Map,如果这里已经相同的cachekey的请求正在请求中
synchronized (mWaitingRequests) {
//mWaitingRequest 是存放同一个cacheKey请求的多个请求。
String cacheKey = request.getCacheKey(); //获取请求的cacheKey
if (mWaitingRequests.containsKey(cacheKey)) {
//已经添加进了队列,那么将request添加到 队列中
// There is already a request in flight. Queue up.
Queue<Request<?>> stagedRequests = mWaitingRequests.get(cacheKey);
if (stagedRequests == null) {
stagedRequests = new LinkedList<>();
}
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里面还不存在,cacheKey的请求,则将cacheKey放入mWaitingRequest并且将当前请求加入mCacheQueue中
mWaitingRequests.put(cacheKey, null);
mCacheQueue.add(request);
}
return request;
}
}
上面我已经添加了对应的注释,首先我们将Request与当前RequestQueue
关联起来,然后将reqeust
加入mCurrentRequests
里面 我们可以看到因为每一个请求都会加进去,所以这个集合就是记录了所有的请求。然后根据request是否可以缓存,如果不可缓存,那么直接添加到NetworkQueue进行网络请求,否则继续往下。 可以缓存,那么先获取到CacheKey
,然后判断mWaitingRequests
中是否存在CacheKey
.等等这个mWaitingRequests
又是什么东东。在我看来,mWaitingRequests
就是存储相同Cachekey
的Request,这样可以避免同一种请求同时被添加进mCacheQueue中,可以在第一个请求结束之后再添加进cacheQueue队列中,这样之后的请求就可以直接从缓存中取,即快速又减少了网络的重复访问。这样是不是很好。当然如果现在mWaitingRequest
里面不包括当前请求CacheKey
那么就添加进mWaitingRequest
,并且将请求添加到mCacheQueue
进行缓存请求。 其实Add方法很简单,就是根据 Request.shouldCache
判断应该进行网络请求还是缓存请求。那么请求添加进去了。你好不好奇,那些重复的Request
什么时候加入CacheQueue
或者怎么处理,那么就应该一起来看看RequestQueue.finish
方法了。
/**
* Called from {@link Request#finish(String)}, indicating that processing of the given request
* has finished.
*
* <p>Releases waiting requests for <code>request.getCacheKey()</code> if
* <code>request.shouldCache()</code>.</p>
*/
<T> void finish(Request<T> request) {
// Remove from the set of requests currently being processed.
//从当前请求集合里面移除要finish掉的请求
synchronized (mCurrentRequests) {
mCurrentRequests.remove(request);
}
//回调finish给Listener
synchronized (mFinishedListeners) {
for (RequestFinishedListener<T> listener : mFinishedListeners) {
listener.onRequestFinished(request);
}
}
//判断request是否shouldCache
if (request.shouldCache()) {
synchronized (mWaitingRequests) {
String cacheKey = request.getCacheKey();
//根据cacheKey 从mWaitingRequests中移除,同时将返回的相同cachekey的请求放入mCacheQueue中,
// 这样只要缓存没过期就可以从缓存中取数据
Queue<Request<?>> waitingRequests = mWaitingRequests.remove(cacheKey);
if (waitingRequests != null) {
if (VolleyLog.DEBUG) {
VolleyLog.v("Releasing %d waiting requests for cacheKey=%s.",
waitingRequests.size(), cacheKey);
}
// Process all queued up requests. They won't be considered as in flight, but
// that's not a problem as the cache has been primed by 'request'.
//从英文就可以知道,因为当前request已经请求过了 所以接下来的请求可以从缓存中拿响应
mCacheQueue.addAll(waitingRequests);
}
}
}
}
看到了吧,相同的CacheKey的请求就是在这里处理的哦。 终于终于把一次完整的StringRequest请求给讲清楚了。在写的过程中,我自己也对这个每一个点越来越理解,所以有时候如果你学习了一个新东西,尽管网上有很多的资料了,但是你自己写一下总结的文章,在写的过程中,潜移默化中你掌握的更深本来以前不理解的地方也更加清晰。所以鼓励大家都写起来,不为其他,为了让自己真正掌握,俗话说好记性不如烂笔头。
哈哈,扯得多了点,但是还没完呢,最后一个流程图,给我和你们一起缕缕整个过程。见流程图。
整体流程图
CacheDispatcher
NetworkDispatcher
好了,各位Volley源码的分析就到此结束了。