1. Http协议中的缓存
1.1 缓存分类
1.1.1 服务端缓存
常见的服务端有Ngix和Apache。服务端缓存又分为代理服务器缓存和反向代理服务器缓存。常见的CDN就是服务器缓存。这个好理解,当浏览器重复访问一张图片地址时,CDN会判断这个请求有没有缓存,如果有的话就直接返回这个缓存的请求回复,而不再需要让请求到达真正的服务地址,这么做的目的是减轻服务端的运算压力
1.1.2 客户端缓存
客户端主要指浏览器(如IE、Chrome等),当然包括我们的OKHTTPClient.客户端第一次请求网络时,服务器返回回复信息。如果数据正常的话,客户端缓存在本地的缓存目录。当客户端再次访问同一个地址时,客户端会检测本地有没有缓存,如果有缓存的话,数据是有没有过期,如果没有过期的话则直接运用缓存内容。
浏览器第一次请求的流程.png 浏览器再次请求时.png1.2 几个概念
1.2.1 Cache-Control
Cache-control是由服务器返回的Response中添加的头信息,它的目的是告诉客户端是要从本地读取缓存还是直接从服务器摘取消息。它有不同的值,每一个值有不同的作用
下图是写简书时抓取到的响应日志
image.pngmax-age:这个参数告诉浏览器将页面缓存多长时间,超过这个时间后才再次向服务器发起请求检查页面是否有更新。对于静态的页面,比如图片、CSS、Javascript,一般都不大变更,因此通常我们将存储这些内容的时间设置为较长的时间,这样浏览器会不会向浏览器反复发起请求,也不会去检查是否更新了。
s-maxage:这个参数告诉缓存服务器(proxy,如Squid)的缓存页面的时间。如果不单独指定,缓存服务器将使用max-age。对于动态内容(比如文档的查看页面),我们可告诉浏览器很快就过时了(max-age=0),并告诉缓存服务器(Squid)保留内容一段时间(比如,s-maxage=7200)。一旦我们更新文档,我们将告诉Squid清除老的缓存版本。
must-revalidate:这告诉浏览器,一旦缓存的内容过期,一定要向服务器询问是否有新版本。
proxy-revalidate:proxy上的缓存一旦过期,一定要向服务器询问是否有新版本。
no-cache:不做缓存。
no-store:数据不在硬盘中临时保存,这对需要保密的内容比较重要。
public:告诉缓存服务器, 即便是对于不该缓存的内容也缓存起来,比如当用户已经认证的时候。所有的静态内容(图片、Javascript、CSS等)应该是public的。
private:告诉proxy不要缓存,但是浏览器可使用private cache进行缓存。一般登录后的个性化页面是private的。
no-transform: 告诉proxy不进行转换,比如告诉手机浏览器不要下载某些图片。
max-stale指示客户机可以接收超出超时期间的响应消息。如果指定max-stale消息的值,那么客户机可以接收超出超时期指定值之内的响应消息。
1.2.2 expires
expires的效果等同于Cache-Control,不过它是Http 1.0的内容,它的作用是告诉浏览器缓存的过期时间,在此时间内浏览器不需要直接访问服务器地址直接用缓存内容就好了。
expires最大的问题在于如果服务器时间和本地浏览器相差过大的问题。那样误差就很大。所以基本上用Cache-Control:max-age=多少秒的形式代替。
1.2.3 Last-Modified/If-Modified-Since
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。
1.2.4 Etag/If-None-Match
Etag对应请求的资源在服务器中的唯一标识(具体规则由服务器决定),比如一张图片,它在服务器中的标识为ETag: W/”ACXbWXd1n0CGMtAd65PcoA==”。
If-None-Match 如果浏览器在Cache-Control:max-age=60设置的时间超时后,发现消息头中还设置了Etag值。然后,浏览器会再次向服务器请求数据并添加In-None-Match消息头,它的值就是之前Etag值。服务器通过Etag来定位资源文件,根据它是否更新的情况给浏览器返回200或者是304。
Etag机制比Last-Modified精确度更高,如果两者同时设置的话,Etag优先级更高。
1.2.5 Pragma
Pragma头域用来包含实现特定的指令,最常用的是Pragma:no-cache。
在HTTP/1.1协议中,它的含义和Cache- Control:no-cache相同。
2. Okhttp缓存使用
2.1 demo
Response的消息有两种类型,CacheResponse和NetworkResponse。CacheResponse代表从缓存取到的消息,NetworkResponse代表直接从服务端返回的消息
private void testCache(){
//缓存文件夹
File cacheFile = new File(getExternalCacheDir().toString(),"cache");
//缓存大小为10M
int cacheSize = 10 * 1024 * 1024;
//创建缓存对象
final Cache cache = new Cache(cacheFile,cacheSize);
new Thread(new Runnable() {
@Override
public void run() {
OkHttpClient client = new OkHttpClient.Builder()
.cache(cache)
.build();
//官方的一个示例的url
String url = "http://publicobject.com/helloworld.txt";
Request request = new Request.Builder()
.url(url)
.build();
Call call1 = client.newCall(request);
Response response1 = null;
try {
//第一次网络请求
response1 = call1.execute();
Log.i(TAG, "testCache: response1 :"+response1.body().string());
Log.i(TAG, "testCache: response1 cache :"+response1.cacheResponse());
Log.i(TAG, "testCache: response1 network :"+response1.networkResponse());
response1.body().close();
} catch (IOException e) {
e.printStackTrace();
}
Call call12 = client.newCall(request);
try {
//第二次网络请求
Response response2 = call12.execute();
Log.i(TAG, "testCache: response2 :"+response2.body().string());
Log.i(TAG, "testCache: response2 cache :"+response2.cacheResponse());
Log.i(TAG, "testCache: response2 network :"+response2.networkResponse());
Log.i(TAG, "testCache: response1 equals response2:"+response2.equals(response1));
response2.body().close();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
其实控制缓存的消息头往往是服务端返回的信息中添加的如”Cache-Control:max-age=60”。所以,会有两种情况。
-
客户端和服务端开发能够很好沟通,按照达成一致的协议,服务端按照规定添加缓存相关的消息头。
-
客户端与服务端的开发根本就不是同一家公司,没有办法也不可能要求服务端按照客户端的意愿进行开发。
第一种办法当然很好,只要服务器在返回消息的时候添加好Cache-Control相关的消息便好。
第二种情况,就很麻烦,你真的无法左右别人的行为。怎么办呢?好在OKHTTP能够很轻易地处理这种情况。那就是定义一个拦截器,人为地添加Response中的消息头,然后再传递给用户,这样用户拿到的Response就有了我们理想当中的消息头Headers,从而达到控制缓存的意图,正所谓移花接木。
2.2 缓存拦截器
class CacheInterceptor implements Interceptor{
@Override
public Response intercept(Chain chain) throws IOException {
Response originResponse = chain.proceed(chain.request());
//设置缓存时间为60秒,并移除了pragma消息头,移除它的原因是因为pragma也是控制缓存的一个消息头属性
return originResponse.newBuilder().removeHeader("pragma")
.header("Cache-Control","max-age=60").build();
}
}
private void testCacheInterceptor(){
//缓存文件夹
File cacheFile = new File(getExternalCacheDir().toString(),"cache");
//缓存大小为10M
int cacheSize = 10 * 1024 * 1024;
//创建缓存对象
final Cache cache = new Cache(cacheFile,cacheSize);
OkHttpClient client = new OkHttpClient.Builder()
.addNetworkInterceptor(new CacheInterceptor())
.cache(cache)
.build();
.......
}
2.3 拦截器缓存的缺点
网上有人说用拦截器进行缓存是野路子,是HOOK行为。这个我不大同意,前面我有分析过情况,如果客户端能够同服务端一起协商开发,当然以服务器控制的缓存消息头为准,但问题在于你没法这样做。所以,能够解决问题才是最实在的。
好了,回到正题。用拦截器控制缓存有什么不好的地方呢?我们先看看下面的情况。
-
网络访问请求的资源是文本信息,如新闻列表,这类信息经常变动,一天更新好几次,它们用的缓存时间应该就很短。
-
网络访问请求的资源是图片或者视频,它们变动很少,或者是长期不变动,那么它们用的缓存时间就应该很长。
那么,问题来了。
因为OKHTTP开发建议是同一个APP,用同一个OKHTTPCLIENT对象这是为了只有一个缓存文件访问入口。这个很容易理解,单例模式嘛。但是问题拦截器是在OKHttpClient.Builder当中添加的。如果在拦截器中定义缓存的方法会导致图片的缓存和新闻列表的缓存时间是一样的,这显然是不合理的,这属于一刀切,就像这两天专家说的要把年收入12万元的人群划分为高收入人群而不区别北上广深的房价物价情况。真实的情况不应该是图片请求有它的缓存时间,新闻列表请求有它的缓存时间,应该是每一个Request有它的缓存时间。
那么,有解决的方案吗?
有的,okhttp官方有建议的方法。
2.3 建议的缓存方法 CacheControl
CacheControl.Builder
- noCache();//不使用缓存,用网络请求
- noStore();//不使用缓存,也不存储缓存
- onlyIfCached();//只使用缓存
- noTransform();//禁止转码
- maxAge(10, TimeUnit.MILLISECONDS);//设置超时时间为10ms。
- maxStale(10, TimeUnit.SECONDS);//超时之外的超时时间为10s
- minFresh(10, TimeUnit.SECONDS);//超时时间为当前时间加上10秒钟。
CacheControl是针对Request的,所以它可以针对每个请求设置不同的缓存策略。比如图片和新闻列表。下面代码展示如何用CacheControl设置一个60秒的超时时间。
private void testCacheControl(){
//缓存文件夹
File cacheFile = new File(getExternalCacheDir().toString(),"cache");
//缓存大小为10M
int cacheSize = 10 * 1024 * 1024;
//创建缓存对象
final Cache cache = new Cache(cacheFile,cacheSize);
new Thread(new Runnable() {
@Override
public void run() {
OkHttpClient client = new OkHttpClient.Builder()
.cache(cache)
.build();
//设置缓存时间为60秒
CacheControl cacheControl = new CacheControl.Builder()
.maxAge(60, TimeUnit.SECONDS)
.build();
Request request = new Request.Builder()
.url("http://blog.csdn.net/briblue")
.cacheControl(cacheControl)
.build();
try {
Response response = client.newCall(request).execute();
response.body().close();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
2.3.1 强制使用缓存
如果缓存不符合条件会返回504.这个时候我们要根据情况再进行编码,如缓存不行就再进行一次网络请求
Request request = new Request.Builder()
.url("http://blog.csdn.net/briblue")
.cacheControl(Cache.FORCE_CACHE)
.build();
Response forceCacheResponse = client.newCall(request).execute();
if (forceCacheResponse.code() != 504) {
// 资源已经缓存了,可以直接使用
} else {
// 资源没有缓存,或者是缓存不符合条件了。
}
2.3.2 不适用缓存
Request request = new Request.Builder()
.url("http://blog.csdn.net/briblue")
.cacheControl(Cache.FORCE_NETWORK)
.build();
还有一种情况将maxAge设置为0,也不会取缓存,直接走网络。
Request request = new Request.Builder()
.url("http://blog.csdn.net/briblue")
.cacheControl(new CacheControl.Builder()
.maxAge(0, TimeUnit.SECONDS))
.build();
3. Cache
To measure cache effectiveness, this class tracks three statistics:
Request Count: the number of HTTP requests issued since this cache was created.
Network Count: the number of those requests that required network use.
Hit Count: the number of those requests whose responses were served by the cache.
Sometimes a request will result in a conditional cache hit. If the cache contains a stale copy of the response, the client will issue a conditional GET
. The server will then send either the updated response if it has changed, or a short 'not modified' response if the client's copy is still valid. Such responses increment both the network count and hit count.
网友评论