磁盘缓存DiskBasedCache

如果你还不知道volley有磁盘缓存的话,请看一下我的另一篇博客请注意,Volley已默认使用磁盘缓存

DiskBasedCache内部结构

它由两部分组成,一部分是头部,一部分是内容;先得从它的内部静态类CacheHeader(缓存的头部信息)讲起,先看它的内部结构:

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里。

DiskBasedCache的使用流程

  • 初始化

    DiskBasedCache的初始化时在RequestQueue新建时就发生的,可以看Volley.newRequestQueue()的源码:

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);

而是需要这样:

    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在做一些基础判断时都会先用到缓存的头部数据,如果确定头部信息没问题了,再真正读写内容,原因是头部数据比较小,放在内存中也不占地方,但处理速度会快很多。而真正的数据内容,可能会比较大,处理的开销也大,只在真正需要的地方读写。

Volley对304的处理

http的304状态码的含义是:

如果服务器端的资源没有变化,则自动返回 HTTP 304 (Not Changed.)状态码,内容为空,这样就节省了传输数据量。当服务器端代码发生改变或者重启服务器时,则重新发出资源,返回和第一次请求时类似。从而 保证不向客户端重复发出资源,也保证当服务器有变化时,客户端能够得到最新的资源。

完整的过程如下:

  1. 客户端请求一个页面(A)。
  2. 服务器返回页面A,并在给A加上一个Last-Modified/ETag。(Last-Modified为标记此文件在服务期端最后被修改的时间,ETag是这个请求的token)
  3. 客户端展现该页面,并将页面连同Last-Modified/ETag一起缓存。
  4. 客户再次请求页面A,并将上次请求时服务器返回的Last-Modified/ETag一起传递给服务器。
  5. 服务器检查该Last-Modified或ETag,并判断出该页面自上次客户端请求之后还未被修改,直接返 回响应304和一个空的响应体。

介绍完304,我们接下来来看看volley是怎么运用304来重用缓存的。

Volley对于头部的解析

首先我们来看一下对于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对于新鲜度和过期的验证

在使用缓存数据前,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 源码解析

Volley(六 )—— 从源码带看Volley的缓存机制的更多相关文章

  1. 从 CPython 源码角度看 Python 垃圾回收机制

    环状双向链表 refchain 在 Python 程序中创建的任何对象都会被放到 refchain 链表中,当创建一个 Python 对象时,内部实际上创建了一些基本的数据: 上一个对象 下一个对象 ...

  2. jquery源码解析:jQuery数据缓存机制详解2

    上一课主要讲了jQuery中的缓存机制Data构造方法的源码解析,这一课主要讲jQuery是如何利用Data对象实现有关缓存机制的静态方法和实例方法的.我们接下来,来看这几个静态方法和实例方法的源码解 ...

  3. jquery源码解析:jQuery数据缓存机制详解1

    jQuery中有三种添加数据的方法,$().attr(),$().prop(),$().data().但是前面两种是用来在元素上添加属性值的,只适合少量的数据,比如:title,class,name等 ...

  4. RecyclerView 源码分析(二) —— 缓存机制

    在前一篇文章 RecyclerView 源码分析(一) -- 绘制流程解析 介绍了 RecyclerView 的绘制流程,RecyclerView 通过将绘制流程从 View 中抽取出来,放到 Lay ...

  5. hbase源码系列(十三)缓存机制MemStore与Block Cache

    这一章讲hbase的缓存机制,这里面涉及的内容也是比较多,呵呵,我理解中的缓存是保存在内存中的特定的便于检索的数据结构就是缓存. 之前在讲put的时候,put是被添加到Store里面,这个Store是 ...

  6. 13 hbase源码系列(十三)缓存机制MemStore与Block Cache

    这一章讲hbase的缓存机制,这里面涉及的内容也是比较多,呵呵,我理解中的缓存是保存在内存中的特定的便于检索的数据结构就是缓存. 之前在讲put的时候,put是被添加到Store里面,这个Store是 ...

  7. [源码分析] 从源码入手看 Flink Watermark 之传播过程

    [源码分析] 从源码入手看 Flink Watermark 之传播过程 0x00 摘要 本文将通过源码分析,带领大家熟悉Flink Watermark 之传播过程,顺便也可以对Flink整体逻辑有一个 ...

  8. robotlegs2.0框架实例源码带注释

    robotlegs2.0框架实例源码带注释 Robotlegs2的Starling扩展 有个老外写了robotleges2的starling扩展,地址是 https://github.com/brea ...

  9. Net 通用权限管理系统源码 带数据库设计文档,部署说明文档

    Net 通用权限管理系统源码 带数据库设计文档,部署说明文档 包括数据库设计文档部署安装文档源码数据库文件 下载地址:http://www.mallhd.com/archives/1389

随机推荐

  1. jekyll

    bundle show minima查看安装路径 bundle exec github-pages versions 建立一个类似于master的分支,与master是完全独立 git checkou ...

  2. SharePoint 2013: Search Architecture in SPC202

    http://social.technet.microsoft.com/wiki/contents/articles/15989.sharepoint-2013-search-architecture ...

  3. Sharepoint学习笔记—习题系列--70-573习题解析 -(Q51-Q53)

    Question 51You use a third-party site definition to create SharePoint sites.You need to add a Web Pa ...

  4. IOS中程序如何进行推送消息(本地推送,远程推送)2(上)

    未看过本地推送的,可以提前看一下本地推送. http://www.cnblogs.com/wolfhous/p/5135711.html =============================== ...

  5. 一些C语言学习的国外资源

    下面的列表是在网上收集整理的C语言资料,包括PDF等多种格式 A Tutorial on Pointers and Arrays in C Beej's Guide to C Programming ...

  6. cell自适应高度

    MyModel.h #import <Foundation/Foundation.h> #import <UIKit/UIKit.h> @interface MyModel : ...

  7. 【原】error C2679: binary '<<' : no operator found which takes a right-hand operand of type 'std::string'

    今天遇到一个非常难以排查的BUG,谷歌度娘都问过了依旧无解,最后自己重新尝试之后找到解决方案: 先看一下报错信息: 1>.\lenz.cpp(2197)  error C2679: binary ...

  8. Runtime 方法替换 和 动态添加实例方法 结合使用

    前言: 方法替换,可以替换任意外部类的方法,而动态添加方法只能实现在被添加类创建的对象里,但是将方法替换和动态添加方法结合使用,可以实现,对任意外部类动态添加需要的方法,这个方法可以是类方法也可以是实 ...

  9. animation of android (2)

    android Interpolator 首先是android系统提供的变换方式: 这些方式将转载一篇文章: 转: http://www.cnblogs.com/mengdd/p/3346003.ht ...

  10. 超链接的那些事(三): 属性target

    a标签的属性之一 target 1. 定义     规定在何处打开链接文档. 如果a标签中有target属性,浏览器将会载入和显示用这个标签的 href 属性命名的.名称与这个目标吻合的框架或者窗口中 ...