我们日常开发经常和网络打交道,从服务器上面获取数据。但是如果我们如果在短时间内多次向服务器请求的数据其实都是一样的,我们是没有必要这么浪费用户的流量的。为了提高用户的体验,我们需要合理使用缓存,要使用缓存就得搞明白缓存的一些相关策略机制,于是就有了这篇文章。
Http的缓存机制
我们可以先看下面的思维导图来简单了解一下Http协议里面的一些缓存机制:
正如上图所示,Http的缓存就仅可用于Get请求。为了方便大家理解上图,这里我简单介绍一下一些缓存相关的重要字段:
Expires:
这个字段是记录着缓存的有效期,如Expires:Wed, 26 Jul 2017 13:18:20 GMT.当客户端发现这个缓存有效期已经过去了,就会重新向服务器请求数据.但是这个字段存在问题,就是客户端本地时间和服务器端时间相差过大,就是出现缓存读取失效的情况.
Cache-Control:
这个字段是在http1.1之后出现了,用于替代Expires字段的作用.如果Cache-Control:max-age=(X)秒和Expires同时存在,那么就会给Cache-Control:max-age(X)秒替代.不过这个字段并没有这么简单,它还有其他重要的字段,我们来看下面的介绍:
<code>[1] max-age:</code>这个上面已经简单介绍了一下,这里再具体说明一下:缓存的内容将在xxx秒后失效,这个选项只在HTTP 1.1可用,优先级比Expires高.简单来说就是判断缓存内容上次访问时间是否比这个max-age要小,小就使用缓存.
<code>[2] no-cache:</code>先不要读取缓存中的文件,向WEB服务器请求验证缓存是否新鲜,新鲜则使用缓存
<code>[3] must-revalidate:</code>作用和相同,但是更为严格.每次请求都校验缓存和服务器源文件,一致就使用缓存,不一致就拿最新
<code>[4] no-store:</code>这个字段很关键,它表示数据不在硬盘中临时保存
<code>[5] only-if-cached:</code>就是在客户端有缓存时就是用客户端的缓存,这个一般都是在无网时使用
<code>[6] max-stale:</code>只要缓存的时间没有超过它(max-stale)指定的时间,就可以加载使用.我们可以在无网络的情况下使用
上面关于<code>no-cache</code>和<code>must-revalidate</code>的内容,我是参考下面这篇文章的:
web性能优化之:no-cache与must-revalidate深入探究
Last-Modified / If-Modified-Since
这是需要有缓存存在才能起作用的字段。
<code>Last-Modified:</code>这个字段是用来标记这个响应资源的最后修改时间。服务器在响应请求时,将会告诉客户端这个响应资源的最后修改时间,如Last-Modified:Thu, 13 Jul 2017 06:31:05 GMT;
<code>If-Modified-Since:</code>当缓存不新鲜时,发现缓存具有Last-Modified声明,服务器就会检查请求头If-Modified-Since(客户端缓存页面数据的最后修改时间),服务器会把这个时间与服务器上实际文件的最后修改时间进行比较。如If-Modified-Since:Mon , 24 Jul 2017 18:53:33 GMT
如果时间一致,那么返回HTTP状态码304,客户端接到之后,就直接把本地缓存文件显示到屏幕上面。
如果时间不一致,就返回HTTP状态码200和新的文件内容,客户端接到之后,会丢弃旧文件,把新文件缓存起来并显示新的内容。
Etag / If-None-Match
这是需要有缓存存在才能起作用的字段,而且<code>Etag</code>的优先级高于<code>Last-Modified</code>。
<code>Etag:</code>这个字段是请求变量的实体标记。简单来说就是服务器响应时给请求URL标记,并在HTTP响应头中将其传送到客户端进行存储。服务器端返回的格式如:Etag:“5d8c72a5edda8d6a:3239″.这是通过对文件的索引节(INode)大小(Size)和最后修改时间(MTime)进行Hash后得到的数值;
<code>If-None-Match:</code>上面说了,<code>Etag</code>是服务器的报文才有的,那么客户端需要给服务器发送Etag应该怎么办?
就是在http报文里面使用If-None-Match这个字段来存放Etag的值.服务器收到请求后发现有If-None-Match则与被请求资源的相应校验串进行比对,之后才决定返回200或304.如:If-None-Match:"5d8c72a5edda8d6a:3239"
Etag 和 Last-Modified
到这里我们会发现一件事,就是<code>Last-Modified</code>和<code>Etag</code>的功能居然是重复的,这就奇怪了。既然使用<code>Last-Modified</code>已经足以让客户端知道本地的缓存副本是否足够新,为什么还需要<code>Etag</code>呢?其实还是有原因的,因为<code>Last-Modified</code>有下面几个问题难以解决:
1.<code>Last-Modified</code>标注的最后修改只能精确到秒级,如果某些文件在1秒钟以内被修改多次的话,它将不能准确标注文件的修改时间
2.如果某些文件会被定期生成,当有时内容并没有任何变化,但<code>Last-Modified</code>却改变了,导致文件没法使用缓存
3.有可能存在服务器没有准确获取文件修改时间或者与代理服务器时间不一致等情形
<code>Etag</code>是服务器自动生成或者由开发者生成的对应资源在服务器端的唯一标识符,能够更加准确的控制缓存;<code>Last-Modified</code>与<code>ETag</code>是可以一起使用的,服务器会优先验证<code>ETag</code>,一致的情况下,才会继续比对<code>Last-Modified</code>,最后才决定是否返回304或200.
通过okhttp的cache设置来加深理解
我们现在已经了解了部分Http的缓存策略,下面我们来通过代码来加深了解吧。本人比较喜欢okhttp,所以下面的代码都是用它来弄了。
public void click(View view) {
int maxCacheSize = 10 * 1024 * 1024;
//设置缓存路径
Cache cache = new Cache(getCacheDir(), maxCacheSize);
OkHttpClient client = new OkHttpClient.Builder()
//设置缓存
.cache(cache)
.build();
Request request = new Request.Builder()
.url("http://www.qq.com/")
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onResponse(Call call, Response response) throws IOException {
Log.w(TAG, "start");
Log.w(TAG, "response cache :" + response.cacheResponse());
Log.w(TAG, "response network :" + response.networkResponse());
response.body().close();
}
@Override
public void onFailure(Call call, IOException e) {
}
});
}
我们先来看下qq官网的Cache-Control:
可以看到max-age=60,说明本地缓存在60秒内都是新鲜的,那么我们就连续两次访问qq官网来看下Log吧:
从打印中,我们可以看出第一次访问我们是从服务器中获取最新的数据;但是第二次访问时,就直接使用本地缓存了,毕竟两次请求相隔时间最多才1秒。那么okhttp缓存到本地时,数据是以什么格式保存的呢?这里我是将缓存保存在内部存储里的:
为了满足好奇心,这里我们来逐一来看下:
原来这3个文件分别是请求的header,加密的response和每一条reponse读写操作状态记录。
ok,我们接着来测试,这次换简书的官网来测试,先看下简书官网的一些header信息:
max-age=0说明缓存有效时间为0秒,但是设置了Etag校验缓存,我们看下请求打印的Log吧:
我们重点看第二次访问的Log,okhttp的cacheResponse可用,而且它的networkResponse返回的http码为304,说明了本地缓存是可用的。
那么,有什么网站是不支持缓存吗?当然有了,知乎是一个很有名气的网站,它就不支持客户端使用缓存:
它已经在Cache-Control里面设置不支持硬盘存储的no-store字段了,我们看下运行的Log:
可以看到,我们已经多次重新访问知乎的首页了,但是每次都只能重新向服务器拿最新的数据。