Volley实现了对请求响应的结果进行磁盘缓存,这样就不用每次都从服务器上请求数据。Volley的缓存是在DiskBasedCache这个类中实现的,它是在创建请求队列的时候被创建出来的。
private static final String DEFAULT_CACHE_DIR = "volley";
File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);
RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
//DEFAULT_DISK_USAGE_BYTES = 5 * 1024 * 1024
public DiskBasedCache(File rootDirectory) {
this(rootDirectory, DEFAULT_DISK_USAGE_BYTES);
}
public DiskBasedCache(File rootDirectory, int maxCacheSizeInBytes) {
mRootDirectory = rootDirectory;
mMaxCacheSizeInBytes = maxCacheSizeInBytes;
}
Volley的默认缓存路径是在APP的cache目录下的volley目录中File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);DEFAULT_CACHE_DIR = "volley"
。缓存的默认大小是5M。
当CacheDispatcher这个线程被启动的时候,就会调用DiskBasedCache.initialize()方法来初始化一遍,从本地缓存文件中来读取缓存数据到内存中。
@Override
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
//Cache线程启动后就开始初始化缓存
mCache.initialize();
...
}
进入initialize()看看是如何初始化缓存的
@Override
public synchronized void initialize() {
//如果缓存的volley目录不存在旧创建
if (!mRootDirectory.exists()) {
if (!mRootDirectory.mkdirs()) {
VolleyLog.e("Unable to create cache dir %s", mRootDirectory.getAbsolutePath());
}
return;
}
//扫描volley目录下的缓存文件
File[] files = mRootDirectory.listFiles();
if (files == null) {
return;
}
//遍历这些缓存文件
for (File file : files) {
try {
//文件大小
long entrySize = file.length();
CountingInputStream cis = new CountingInputStream(
new BufferedInputStream(createInputStream(file)), entrySize);
try {
//读取缓存文件中的数据
CacheHeader entry = CacheHeader.readHeader(cis);
entry.size = entrySize;
//将数据保存进map集合中
putEntry(entry.key, entry);
} finally {
cis.close();
}
} catch (IOException e) {
file.delete();
}
}
}
这里使用了CacheHeader这个类来描述每一个缓存文件的头部信息,在CacheHeader中有如下描述字段:
//缓存文件大小
public long size;
//缓存对应的标识,一般为request请求的url
public String key;
//与缓存相关的header标签,一般标识一个服务器资源的hash tag
public String etag;
//收到响应的时间
public long serverDate;
//资源文件最后修改的时间
public long lastModified;
//缓存过期的时间
public long ttl;
//整个比较特别,可以理解为缓存可能失效的时间
public long softTtl;
//响应中的头部
public Map<String, String> responseHeaders;
Volley的缓存信息中,ttl和softTtl这连个失效时间容易混淆,ttl还好说,就是失效时间,只要时间已过,就会走网络请求这一条路,不会使用缓存。softTtl表示缓存可能失效了,但仍然继续使用缓存,不过还需要走网络请求这一条路在更新一次。看看代码就清楚了。
//CacheDiapatcher.java
@Override
public void run() {
...
while (true) {
try {
...
//缓存过期,直接走网络请求,都不同缓存数据
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 {
...
//缓存可能过期了,先将就一下用着吧
mDelivery.postResponse(request, response, new Runnable() {
@Override
public void run() {
try {
//这里再走一边网络请求
mNetworkQueue.put(request);
} catch (InterruptedException e) {
}
}
});
}
...
}
}
//Entry
public boolean isExpired() {
return this.ttl < System.currentTimeMillis();
}
public boolean refreshNeeded() {
return this.softTtl < System.currentTimeMillis();
}
在CacheDispatcher中处理缓存请求的时候,通过entry.isExpired()判断缓存数据如果过期了就直接走网络请求,通过entry.refreshNeeded()判断如果缓存可能过期了会先使用缓存数据,再走一遍网络请求。
CacheHeader只保存了缓存数据的关键头信息,每次获取真正的缓存数据都是从文件中读取的。获取缓存数据是在get()里面完成的。
@Override
public synchronized Entry get(String key) {
//1.获取缓存数据头信息
CacheHeader entry = mEntries.get(key);
//2.没有则返回null
if (entry == null) {
return null;
}
//获取缓存文件,这里的key实际上就是请求的url,这里会通过这个url计算出一个hash值来作为文件名称
File file = getFileForKey(key);
CountingInputStream cis = null;
try {
//3.CountingInputStream是对FileInputStream的封装,里面记录了读取的数据大小
cis = new CountingInputStream(new BufferedInputStream(new FileInputStream(file)));
//4.跳过对缓存数据的头信息
CacheHeader.readHeader(cis); // eat header
//5.获取真正的缓存数据
byte[] data = streamToBytes(cis, (int) (file.length() - cis.bytesRead));
//6.返回一个Entry对象,缓存数据就放在了这个对象
return entry.toCacheEntry(data);
} catch (IOException e) {
remove(key);
return null;
} catch (NegativeArraySizeException e) {
remove(key);
return null;
} finally {
if (cis != null) {
try {
cis.close();
} catch (IOException ioe) {
return null;
}
}
}
}
对缓存数据的一些描述信息,比如失效时间、文件大小等都是直接保存在文件的开头的,所以在获取缓存数据的时候需要跳过这些头。Entry对象除了包含了CacheHeader这些头信息,还包含了实际的缓存数据。
public static class Entry {
//缓存数据
public byte[] data;
public String etag;
public long serverDate;
public long lastModified;
public long ttl;
public long softTtl;
public Map<String, String> responseHeaders = Collections.emptyMap();
public boolean isExpired() {
return this.ttl < System.currentTimeMillis();
}
public boolean refreshNeeded() {
return this.softTtl < System.currentTimeMillis();
}
}
既然获取缓存数据是在get()中,那么应该猜到了添加一个缓存数据就是在put()中了。
@Override
public synchronized void put(String key, Entry entry) {
//1.检查缓存空间是否还能放入数据,不行就删掉一些
pruneIfNeeded(entry.data.length);
//2.声明定义一个缓存文件
File file = getFileForKey(key);
try {
BufferedOutputStream fos = new BufferedOutputStream(new FileOutputStream(file));
//3.读取缓存entry中的缓存描述头信息
CacheHeader e = new CacheHeader(key, entry);
//4.写入头信息
boolean success = e.writeHeader(fos);
if (!success) {
fos.close();
VolleyLog.d("Failed to write header for %s", file.getAbsolutePath());
throw new IOException();
}
//5.写入缓存数据
fos.write(entry.data);
fos.close();
//6.将缓存的头信息保存在map中
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%的缓存空间。
private void pruneIfNeeded(int neededSpace) {
//如果还能放入数据就退出
if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes) {
return;
}
long before = mTotalSize;
int prunedFiles = 0;
long startTime = SystemClock.elapsedRealtime();
Iterator<Map.Entry<String, CacheHeader>> iterator = mEntries.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, CacheHeader> entry = iterator.next();
CacheHeader e = entry.getValue();
//从第一个缓存文件开始删除
boolean deleted = getFileForKey(e.key).delete();
if (deleted) {
mTotalSize -= e.size;
} else {
VolleyLog.d("Could not delete cache entry for key=%s, filename=%s",
e.key, getFilenameForKey(e.key));
}
iterator.remove();
prunedFiles++;
//如果删除后缓存空间剩余10%以上就不删除了
if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes * HYSTERESIS_FACTOR) {
break;
}
}
}
读取缓存和保存缓存已经说了,那么这些缓存数据的描述头信息是在哪里处理的呢。Volley默认提供了好几种Request,在这些Request中都会实现parseNetworkResponse()方法,
protected Response<T> parseNetworkResponse(NetworkResponse response)
该方法是将一个NetworkResponse对象转换为Response对象,在其内部都会调用一个方法HttpHeaderParser.parseCacheHeaders(response)
,它会从NetworkResponse中读取这些缓存的关键描述头信息,然后返回一个Entry对象,这样就可以调用DiskBasedCache的put()方法来保存缓存数据了。进入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;
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")) {
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;
}
}
}
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");
if (hasCacheControl) {
softExpire = now + maxAge * 1000;
finalExpire = mustRevalidate
? softExpire
: softExpire + staleWhileRevalidate * 1000;
} else if (serverDate > 0 && serverExpires >= serverDate) {
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;
}
在parseCacheHeaders()内部实际就是解析相应中的响应头信息,找到对应的关键信息保存在Entry对象中返回的。
Volley的缓存机制大致就说这么多了。