一、 HTTP缓存技术介绍
在探究Okhttp的CacheIntercepter原理之前,先简单概述一下http的网络缓存相关知识,方便阅读本文。以请求https://github.com/为例
可以看到返回的response中有许多头信息,但是暂时只需要关注Cache-Control
和Last-modified这两个字段,因为它俩是控制要不要缓存当前内容。
简单介绍一下作用:
1.Cache-control的作用
Cache-control是由服务器返回的Response中添加的头信息,它的目的是告诉客户端是要从本地读取缓存还是直接从服务器摘取消息。它有不同的值,每一个值有不同的作用。
- max-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消息的值,那么客户机可以接收超出超时期指定值之内的响应消息。
当然,这些头信息在Okhttp中当然也可以进行配置
2 Last-Modified
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。
二、Okhttp的CacheInterceptor原理分析
知道了http的缓存相关知识,现在探索CacheInterceptor的原理,以下是核心代码:
Response intercept(Chain chain) throws IOException {
//如果在初始化中配置了cache(),则在每次请求时优先从缓存中读取Response
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
long now = System.currentTimeMillis();
//缓存策略,通过判断响应头信息和缓存时间标胶来判断缓存是否有效
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
//返回包装后的Request和Response
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
......
//如果根据缓存策略strategy禁止使用网络,并且缓存无效,直接返回空的Response
if (networkRequest == null && cacheResponse == null) {
return new Response.Builder()
......
.code(504)
.message("Unsatisfiable Request (only-if-cached)")
.body(Util.EMPTY_RESPONSE)//空的body
......
.build();
}
//如果根据缓存策略strategy禁止使用网络,且有缓存则直接使用缓存
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
//需要网络
Response networkResponse = null;
try {//执行下一个拦截器,发起网路请求
networkResponse = chain.proceed(networkRequest);
} finally {
......
}
//本地有缓存,
if (cacheResponse != null) {
//并且服务器返回304状态码(说明缓存还没过期或服务器资源没修改)
if (networkResponse.code() == HTTP_NOT_MODIFIED) {
//使用缓存数据
Response response = cacheResponse.newBuilder()
......
.build();
......
//返回缓存
return response;
} else {
closeQuietly(cacheResponse.body());
}
}
//如果网络资源已经修改:使用网络响应返回的最新数据
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
//将最新的数据缓存起来
if (cache != null) {
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
CacheRequest cacheRequest = cache.put(response);
return cacheWritingResponse(cacheRequest, response);
}
......
//返回最新的数据
return response;
}
以上就是CacheInterceptor的核心代码,写了部分注释,简单描述一下整体思路:
1.假如在okhhtpClient中配置了缓存,即调用了
Cache cache = new Cache(new File(MainActivity.this.getApplication().getCacheDir(), "HttpCache"), 1024 * 1024 * 100);
OkHttpClient okHttpClient = new OkHttpClient().newBuilder()
.cache(cache)
.build();
所有的网络请求都会去缓存中获取内容
2.将网络请求request和缓存response进行封装为CacheStrategy对象
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
看看这句话做了什么事?通过下面可以看出,将缓存的Response的中的信息头进行替换为最新的信息
public Factory(long nowMillis, Request request, Response cacheResponse) {
this.nowMillis = nowMillis;
this.request = request;
this.cacheResponse = cacheResponse;
if (cacheResponse != null) {
this.sentRequestMillis = cacheResponse.sentRequestAtMillis();
this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis();
Headers headers = cacheResponse.headers();
for (int i = 0, size = headers.size(); i < size; i++) {
String fieldName = headers.name(i);
String value = headers.value(i);
if ("Date".equalsIgnoreCase(fieldName)) {
servedDate = HttpDate.parse(value);
servedDateString = value;
} else if ("Expires".equalsIgnoreCase(fieldName)) {
expires = HttpDate.parse(value);
} else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
lastModified = HttpDate.parse(value);
lastModifiedString = value;
} else if ("ETag".equalsIgnoreCase(fieldName)) {
etag = value;
} else if ("Age".equalsIgnoreCase(fieldName)) {
ageSeconds = HttpHeaders.parseSeconds(value, -1);
}
}
}
}
public CacheStrategy get() {
CacheStrategy candidate = getCandidate();
......
return candidate;
}
getCandidate()中做的事:
String conditionName;
String conditionValue;
if (etag != null) {
conditionName = "If-None-Match";
conditionValue = etag;
} else if (lastModified != null) {
conditionName = "If-Modified-Since";
conditionValue = lastModifiedString;
} else if (servedDate != null) {
conditionName = "If-Modified-Since";
conditionValue = servedDateString;
}
Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();
//将缓存中的请求头信息,重新拼装到request上
//addLenient方法调用okhttpClient的实现,则最终是拼接头信息。
Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);
3.从CacheStrategy中获取到netRequest和cacheResponse进行相关的判断
- 3.1 假如netRequest和cacheResponse都为空,即设置的请求为只能从缓存中获取,但是缓存中并没有该资源,这种情况下,则会直接返回一个
if (networkRequest == null && cacheResponse == null) {
return new Response.Builder()
.request(chain.request())
.protocol(Protocol.HTTP_1_1)
.code(504)
.message("Unsatisfiable Request (only-if-cached)")
.body(Util.EMPTY_RESPONSE)
.sentRequestAtMillis(-1L)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
}
- 3.2 假如request为空,则说明配置了从缓存中进行获取则直接放回缓存的response 即:
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
-
3.3 如果networkRequest不为空,则需要重新请求网络,得到networkResponse
- 3.3.1 如果networkResponse的状态码是为HTTP_NOT_MODIFIED即304(表示资源未发生改变),并且本地有缓存,则可以直接返回缓存的内容即cacheResponse
- 3.3.2 如果networkResponse的状态码不为304,则直接返回networkResponse,
4. 根据是否设置了缓存策略,根据
-
4.1 .HttpHeaders.hasBody(response)判断Response的状态码是否返回正常和响应的内容不为空
-
4.2 CacheStrategy.isCacheable(response,networkRequest)判断是否进行缓存,
5.根据4中提到的条件执行以下内容:
- 5.1则将response缓存起来
CacheRequest cacheRequest = cache.put(response);
return cacheWritingResponse(cacheRequest, response);
-
5.2 返回网络请求返回的响应体。
这里有一张图,帮助理解上面的流程:
20170615185405523的副本.png
三、项目中如何使用
Okhttp的缓存拦截器到这里就介绍完了,介绍完它的实现,再来看看再项目中如何定义适合自己的拦截器:
1. 控制缓存的消息头往往是服务端返回的信息中添加的如”Cache-Control:max-age=60”。所以,会有两种情况。
-
1.1 客户端和服务端开发能够很好沟通,按照达成一致的协议,服务端按照规定添加缓存相关的消息头。 客户端只要添加上缓存
-
1.2 客户端与服务端的开发根本就不是同一家公司或者是调用第三方的服务,没有办法也不可能要求服务端按照客户端的意愿进行开发。这个时候想要使用缓存,就需要自己实现请求缓存,即定义一个拦截器,手动添加头信息,达到缓存的效果。
2. 如何让CacheInterceptor发挥作用?
自定义拦截器,将网络请求的数据缓存下来,所谓缓存,就是将请求头中的信息进行设置:即
private static final Interceptor cacheIntercepr = new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Response response = chain.proceed(chain.request());
response.newBuilder().removeHeader("pragma")// 清除头信息,因为服务器如果不支持,会返回一些干扰信息,不清除下面无法生效
.removeHeader("Cache-Control")
.header("Cache-Control", "public, max-age=60").build();
return response;
}
};
清除响应体中的pragma头信息,并且自添加Cache-Control关键字,并且设置为public 并且过期时间为60s,开始验证,以请求http://www.wanandroid.com为例:
正常的一次请求是没有设置任何的缓存,这里通过上面的自定义的拦截器,实现第一次进行请求网络,第二次去请求这个网址的时候,去缓存中获取相关的数据。
new Thread(new Runnable() {
@Override
public void run() {
Cache cache = new Cache(new File(MainActivity.this.getApplication().getCacheDir(), "HttpCache"), 1024 * 1024 * 100);
OkHttpClient okHttpClient = new OkHttpClient().newBuilder()
.addInterceptor(cacheIntercepr)
.cache(cache)
.retryOnConnectionFailure(true)
.build();
//设置第一个请求强制请求网络 CacheControl.FORCE_NETWORK).
Request request = new Request.Builder().cacheControl(CacheControl.FORCE_NETWORK).url("http://www.wanandroid.com").build();
Call call = okHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.d("TAG", "onFailure: ." + e.getMessage().toString());
}
@Override
public void onResponse(Call call, Response response) throws IOException {
Logger.d(response.body().string());
Logger.d("OKHTTP first cacheResponse:" + response.cacheResponse());
Logger.d("OKHTTP first networkResponse:" + response.networkResponse());
}
});
//设置第二个请求强制从缓存中获取 CacheControl.FORCE_CACHE).
Request request2 = new Request.Builder().
cacheControl(CacheControl.FORCE_CACHE).
url("http://www.wanandroid.com")
.build();
Call call2 = okHttpClient.newCall(request2);
call2.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.d("TAG", "onFailure: ." + e.getMessage().toString());
}
@Override
public void onResponse(Call call, Response response) throws IOException {
Logger.d(response.body().string());
Logger.d("OKHTTP second cacheResponse:" + response.cacheResponse());
Logger.d("OKHTTP second networkResponse:" + response.networkResponse());
}
});
}
}).start();
第一次请求.png
第二次请求.png
如果不设置缓存,则每次请求都会去请求网络,缓存技术其实在软件开发中随时可见,尤其是高并发的应用中,更能体现出缓存的重要性,缓存技术不仅在请求网络中使用,涉及到很多方面,比如数据库中也有用到缓存技术,解决的方法就是把重复请求的数据缓存在本地,并设置超时时间,在规定时间内,客户端不再向远程请求数据,而是直接从本地缓存中取数据。
四、常用API
1.Okhttp中建议用CacheControl这个类来进行设置缓存策略
当然这是在Request类上面使用,配置示例:
Request request = new Request.Builder().cacheControl(CacheControl.FORCE_NETWORK).url("http://www.wanandroid.com").build();
常用策略:
- noCache();//不使用缓存,用网络请求
- noStore();//不使用缓存,也不存储缓存
- onlyIfCached();//只使用缓存
- noTransform();//禁止转码
- maxAge(10, TimeUnit.MILLISECONDS);//设置超时时间为10ms。
- maxStale(10, TimeUnit.SECONDS);//超时之外的超时时间为10s
- minFresh(10, TimeUnit.SECONDS);//超时时间为当前时间加上10秒钟。
2.强制使用网络请求
无论缓存策略如何配置,都会进行网络请求
public static final CacheControl FORCE_NETWORK = new Builder().noCache().build();
3. 强制性使用本地缓存
请求只会从缓存中获取数据
public static final CacheControl FORCE_CACHE = new Builder()
.onlyIfCached()
.maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS)
.build();
网友评论