看过的人都知道,Volley的结构其实很简单。我自己也这样觉得,对于缓存的逻辑一直有点问题。今天抽时间好好总结了一番。

Volley的基本结构

简单来说,Volley维护了两个队列,CacheQueue 和 NetworkQueue。同时在初始化的时候,启动处理线程(CacheDispacher 和 NetworkDispacher)来不停的从队列中取出请求并处理。

CacheDispacher做的工作是检验是否有缓存,缓存是否过期,如果过期的话,就把它放进请求队列,如果有缓存而且没有过期,那么就使用缓存。

NetworkDispacher做的工作也很简单,取出Request以后,就通过BasicNetwork进行请求,BasicNetwork调用HttpStack就行请求,HttpStack调用HttpURLConnection或者HttpClient进行请求。

其实都非常的简单,所以Volley其实就是在传统网络请求HttpURLConnection这些组件上层的一个包装。

不能不提的Cache-Control

在Response.success方法中,要传入一个参数,Cache.Entry类型。

在StringRequest文件中,这个对象是由parseCacheHeadlers决定的。换句话说,是由请求来的响应报文决定的。

1
2
3
4
5
@Override
protected Response<String> parseNetworkResponse(NetworkResponse response) {
String parsed = ...;
return Response.success(parsed, HttpHeaderParser.parseCacheHeaders(response));
}

这个对象记录着对于从网络加载来的信息的缓存情况。那么它到底记录了哪些信息?这些信息又是如何决定什么时候使用缓存,什么时候缓存失效?这就是今天要讨论的Cache-Control

如果在响应报文中按照下面的设置,会让客户端对响应内容缓存3600秒,也即在3600秒内,如果客户再次访问该资源,直接从客户端的缓存中返回内容给客户,不要再从服务端获取
(当然,这个功能是靠客户端实现的,服务端只是通过这个属性提示客户端“应该这么做”,做不做,还是决定于客户端,如果是自己宣称支持HTTP的客户端,则就应该这样实现。所以Volley肯定这样实现呀)。

Cache-Control: max-age=3600

http协议头Cache-Control的值可以是public、private、no-cache、no- store、no-transform、must-revalidate、proxy-revalidate、max-age、stale-while-revalidate

各个消息中的指令含义如下:

  • public HTTP响应可以由任何缓存来缓存。例如,客户端或代理服务器都可以缓存响应。这允许在使用同一代理服务器的用户之间共享内容。

  • private 此响应消息专门针对单个客户端,不能由共享缓存进行缓存。例如,代理服务器不应缓存响应,而客户端则可以。这就使得一台客户端可以保留一个缓存版本,而使用同一代理服务器的其他客户端可以保留不同的缓存版本。

  • no-cache 告知缓存者,必须原原本本的转发原始请求,并告知任何缓存者,别直接拿你缓存的副本

  • no-store 语义十分强烈,请求和响应都禁止被缓存.(也许是出于隐私考虑)
  • max-age 指示客户端能够使用缓存的时间(以秒为单位)
  • must-revalidate 与max-age配合使用,过了max-age缓存时间,就立即请求服务器数据(死刑,立即执行)
  • stale-while-revalidate 与max-age配合使用,过了max-age缓存时间,还可以在使用缓存(死缓,缓期就是stale-while-revalidate设置的时间。真正使用缓存的时间是max-age + stale-while-revalidate )

ps:过了must-revalidate缓存时间,并且没有过了stale-while-revalidate的时间,这个期间是不新鲜(refresh)的缓存。所以通常客户端会先把缓存交给用户,同时向服务端发送网络请求,来获取新鲜的数据。然后把新鲜数据在缓存一遍。

Expires

Expires 与 Cache-Control 的作用一致,都是指明当前资源的 有效期 ,控制浏览器是否直接从浏览器缓存取数据还是重新发请求到服务器取数据。

Expires: Mon, 26 Jul 1997 05:00:00 GMT
只不过Cache-Control的 选择更多,设置更细致 ,如果同时设置的话,其 优先级高于 Expires 。
为什么使用Expires呢?这个是个历史问题,在HTTP1.0的时候,使用Expires控制缓存时间的,这个不太灵活。。
在HTTP1.1的时候,就使用Cache-Control:max-age来控制缓存。一般为了兼容HTTP1.0和1.1,Expires和CacheControl都会加上。

Last-Modified/If-Modified-Since

Last-Modified/If-Modified-Since要配合Cache-Control使用。

Last-Modified:标示这个响应资源的最后修改时间。web服务器在响应请求时,告诉浏览器资源的最后修改时间。

If-Modified-Since:当资源过期时(使用Cache-Control标识的max-age判断),发现资源具有Last-Modified声明,则再次向web服务器请求时带上头 If-Modified-Since,表示请求时间。web服务器收到请求后发现有头If-Modified-Since 则与被请求资源的最后修改时间进行比对。若最后修改时间较新,说明资源又被改动过,则响应整片资源内容(写在响应消息包体内),HTTP 200;若最后修改时间较旧,说明资源无新修改,则响应HTTP 304 (无需包体,节省浏览),告知浏览器继续使用所保存的cache。

Etag/If-None-Match

Etag/If-None-Match也要配合Cache-Control使用。

Etag:web服务器响应请求时,告诉浏览器当前资源在服务器的唯一标识(生成规则由服务器决定)。Apache中,ETag的值,默认是对文件的索引节(INode),大小(Size)和最后修改时间(MTime)进行Hash后得到的。

If-None-Match:当资源过期时(使用Cache-Control标识的max-age判断),发现资源具有Etag声明,则再次向web服务器请求时带上头If-None-Match (Etag的值)。web服务器收到请求后发现有头If-None-Match则与被请求资源的相应校验串进行比对,决定返回200或304。

既生Last-Modified何生Etag?

你可能会觉得使用Last-Modified已经足以让浏览器知道本地的缓存副本是否足够新,为什么还需要Etag(实体标识)呢?HTTP1.1中Etag的出现主要是为了解决几个Last-Modified比较难解决的问题:

Last-Modified标注的最后修改只能精确到秒级,如果某些文件在1秒钟以内,被修改多次的话,它将不能准确标注文件的修改时间

如果某些文件会被定期生成,当有时内容并没有任何变化,但Last-Modified却改变了,导致文件没法使用缓存

有可能存在服务器没有准确获取文件修改时间,或者与代理服务器时间不一致等情形

Etag是服务器自动生成或者由开发者生成的对应资源在服务器端的唯一标识符,能够更加准确的控制缓存。Last-Modified与ETag是可以一起使用的,服务器会优先验证ETag,一致的情况下,才会继续比对Last-Modified,最后才决定是否返回304。

写在最后

值得庆幸的是,这一切,都已经被Volley实现过一遍了。所以我们才会看到NetworkDispacher里请求后,庞杂的if判断。

有时候真的想,写个客户端实在不算什么。不牵涉到点复杂的东西,不牵涉到那种缜密如网络协议的设计,那写代码就没有什么意思了。

还是要提高对自己的要求,对自己就要求全责备才好。