缓存分类
http请求有服务端和客户端之分。因此缓存也可以分为两个类型服务端侧和客户端侧。
- 缓存——服务端
常见的服务端有Ngix和Apache。服务端缓存又分为代理服务器缓存和反向代理服务器缓存(了解就好了)。 - 缓存——客户端
客户端很多种,这里就不多说了。主要说说OkHttpClient。缓存其实就是为了下次请求时节省请求时间,可以更快的展示数据
同时也有更好的用户体验。
常见控制缓存的HTTP头信息
- Expires策略:在多长时间内是有效的。过了这个时间,缓存器就会向服务器发送请求,检验文档是否被修改。该属性对设置静态图片文件缓存特别有用。
- Cache-Control策略:一组头信息属性。通过这个属性可以让发布者全面控制内容,并定位过期时间的限制。
- Last-Modified/If-Modified-Since:Last-Modified/If-Modified-Since要配合Cache-Control使用。
- ETag:服务器生成的唯一标识符ETag,每次副本的标签都会变化。客户端通过ETag询问服务器端资源是否改变。表示服务器返回的一个资源标识,下次客户端请求时将该值作为 key 为 If-None-Match 的值传给服务器判断,如果ETag没改变,则返回状态304。Etag/If-None-Match,这个也需要配合Cache-Control使用。
下面就说说我对OkHttpClient缓存的理解,如有做得不好之处,请多多指教,谢谢!
不多说直接上代码:
private void testOkHttpCache(){
Log.e("TAG",mContext.getCacheDir().toString());
/**
* OKHTTP如果要设置缓存,首要的条件就是设置一个缓存文件夹,在android中为了安全起见,
* 一般设置为私密数据空间。getCacheDir()获取。
*/
//缓存文件夹
File cacheFile = new File(mContext.getCacheDir().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)//开启缓存
.connectTimeout(15,TimeUnit.SECONDS)
.readTimeout(15,TimeUnit.SECONDS)
.build();
//缓存设置
CacheControl cacheControl = new CacheControl.Builder()
.maxAge(3*60, TimeUnit.SECONDS)
.maxStale(3*60, TimeUnit.SECONDS)
// .noCache()//不使用缓存,用网络请求,即使有缓存也不使用
// .noStore()//不使用缓存,也不存储缓存
// .onlyIfCached()//只使用缓存
.build();
Request request = new Request.Builder()
.url("http://img15.3lian.com/2015/h1/280/d/5.jpg")
.cacheControl(cacheControl)
.build();
Response response1 =null;
try {
response1 = client.newCall(request).execute();
// Log.e("TAG", "testCache: response1 :"+response1.body().string());
Log.e("TAG", "testCache: response1 cache :"+response1.cacheResponse());
Log.e("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.e("TAG", "testCache: response2 :"+response2.body().string());
Log.e("TAG", "testCache: response2 cache :"+response2.cacheResponse());
Log.e("TAG", "testCache: response2 network :"+response2.networkResponse());
Log.e("TAG", "testCache: response1 equals response2:"+response2.equals(response1));
response2.body().close();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
运行的结果
07-03 12:15:03.864 17030-17286/com.lu.mystudy E/TAG: testCache: response1 cache :null
07-03 12:15:03.864 17030-17286/com.lu.mystudy E/TAG: testCache: response1 network :Response{protocol=http/1.1, code=200, message=OK, url=http://img15.3lian.com/2015/h1/280/d/5.jpg}
07-03 12:15:04.033 17030-17286/com.lu.mystudy E/TAG: testCache: response2 cache :Response{protocol=http/1.1, code=200, message=OK, url=http://img15.3lian.com/2015/h1/280/d/5.jpg}
07-03 12:15:04.033 17030-17286/com.lu.mystudy E/TAG: testCache: response2 network :null
07-03 12:15:04.033 17030-17286/com.lu.mystudy E/TAG: testCache: response1 equals response2:false
OKHTTP 的缓存原理?
OkHttp缓存,首先得在OkHttpClient#cache配置Cache,即给一个Cache对象。
//创建缓存对象
final Cache cache = new Cache(cacheFile,cacheSize);
本地是否有缓存?
cache 就是在 OkHttpClient.cache(cache) 配置的对象,该对象内部是使用 DiskLruCache 实现的。
底层使用的是 DiskLruCache 缓存机制,从 Cache 的构造中可以验证。源码如下:
public Cache(File directory, long maxSize) {
this(directory, maxSize, FileSystem.SYSTEM);
}
Cache(File directory, long maxSize, FileSystem fileSystem) {
this.cache = DiskLruCache.create(fileSystem, directory, VERSION, ENTRY_COUNT, maxSize);
}
OkHttp的缓存Cache-Control相关头相息
一组头信息属性。通过这个属性可以让发布者全面控制内容,并定位过期时间的限制。
- 看看如上代码缓存的头信息
http://img15.3lian.com/2015/h1/280/d/5.jpg
GET
0
HTTP/1.1 200 OK
9
Content-Type: image/jpeg
Last-Modified: Sat, 09 Jan 2016 03:03:53 GMT
Accept-Ranges: bytes
ETag: "7f90895f8a4ad11:0"
Server: Microsoft-IIS/8.5
Date: Mon, 03 Jul 2017 04:15:02 GMT
Content-Length: 99982
OkHttp-Sent-Millis: 1499055303788
OkHttp-Received-Millis: 1499055303850
不难知道OkHttp缓存通过CacheControl类配置,CacheControl指定请求和响应遵循的缓存机制。
CacheControl.java的介绍
- 两个CacheControl常量
CacheControl.FORCE_CACHE; //仅仅使用缓存
CacheControl.FORCE_NETWORK;// 仅仅使用网络
源码如下:
/**
* Cache control request directives that require network validation of responses. Note that such
* requests may be assisted by the cache via conditional GET requests.
*/
public static final CacheControl FORCE_NETWORK = new Builder().noCache().build();
/**
* Cache control request directives that uses the cache only, even if the cached response is
* stale. If the response isn't available in the cache or requires server validation, the call
* will fail with a {@code 504 Unsatisfiable Request}.
*/
public static final CacheControl FORCE_CACHE = new Builder()
.onlyIfCached()
.maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS)
.build();
- 其他属性
isPublic; 指示响应可被任何缓存区缓存。告诉缓存服务器, 即便是对于不该缓存的内容也缓存起来,比如当用户已经认证的时候。所有的静态内容(图片、Javascript、CSS等)应该是public的。
isPrivate; 指示对于单个用户的整个或部分响应消息,不能被共享缓存处理。
noCache();//不使用缓存,用网络请求
noStore();//不使用缓存,也不存储缓存
onlyIfCached();//只使用缓存
noTransform();//禁止转码
maxAge(10, TimeUnit.MILLISECONDS);//设置超时时间为10ms。
maxStale(10, TimeUnit.SECONDS);//超时之外的超时时间为10s
minFresh(10, TimeUnit.SECONDS);//超时时间为当前时间加上10秒钟。
主要是的接入点—— CacheInterceptor#intercept(),源码如下:
从缓存服务请求,并编写对缓存的响应。该拦截器用于处理缓存的功能,主要取得缓存 response 返回并刷新缓存。
@Override public Response intercept(Chain chain) throws IOException {
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
long now = System.currentTimeMillis();
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
if (cache != null) {
cache.trackResponse(strategy);
}
if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
}
// If we're forbidden from using the network and the cache is insufficient, fail.
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();
}
// If we don't need the network, we're done.
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
Response networkResponse = null;
try {
networkResponse = chain.proceed(networkRequest);
} finally {
// If we're crashing on I/O or otherwise, don't leak the cache body.
if (networkResponse == null && cacheCandidate != null) {
closeQuietly(cacheCandidate.body());
}
}
// If we have a cache response too, then we're doing a conditional get.
if (cacheResponse != null) {
if (networkResponse.code() == HTTP_NOT_MODIFIED) {
Response response = cacheResponse.newBuilder()
.headers(combine(cacheResponse.headers(), networkResponse.headers()))
.sentRequestAtMillis(networkResponse.sentRequestAtMillis())
.receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
networkResponse.body().close();
// Update the cache after combining headers but before stripping the
// Content-Encoding header (as performed by initContentStream()).
cache.trackConditionalCacheHit();
cache.update(cacheResponse, response);
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)) {
// Offer this request to the cache.
CacheRequest cacheRequest = cache.put(response);
return cacheWritingResponse(cacheRequest, response);
}
if (HttpMethod.invalidatesCache(networkRequest.method())) {
try {
cache.remove(networkRequest);
} catch (IOException ignored) {
// The cache cannot be written.
}
}
}
return response;
}
CacheStrategy
给定一个请求和缓存响应,这将决定是否使用网络、缓存或两者。内部有两个属性:networkRequest和cacheResponse,在 CacheStrategy 内部会对这个两个属性在特定的情况赋值。
/** The request to send on the network, or null if this call doesn't use the network. */
public final Request networkRequest;//若是不为 null ,表示需要进行网络请求
/** The cached response to return or validate; or null if this call doesn't use a cache. */
public final Response cacheResponse;//若是不为 null ,表示可以使用本地缓存
private CacheStrategy(Request networkRequest, Response cacheResponse) {
this.networkRequest = networkRequest;
this.cacheResponse = cacheResponse;
}
获取一个CacheStrategy
- CacheStrategy strategy = new CacheStrategy.Factory(xxx,xxx, xxx).get();
public Factory(long nowMillis, Request request, Response cacheResponse) {
this.nowMillis = nowMillis;
this.request = request;
this.cacheResponse = cacheResponse;
if (cacheResponse != null) {
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 = HeaderParser.parseSeconds(value, -1);
} else if (OkHeaders.SENT_MILLIS.equalsIgnoreCase(fieldName)) {
sentRequestMillis = Long.parseLong(value);
} else if (OkHeaders.RECEIVED_MILLIS.equalsIgnoreCase(fieldName)) {
receivedResponseMillis = Long.parseLong(value);
}
}
}
}
Factory(long nowMillis, Request request, Response cacheResponse)方法中对应的参数 :
- nowMillis 当前时间。
- request 请求对象。
- cacheResponse 从缓存中取出的 Response 对象。
在Factory方法中判断cacheResponse ,如果cacheResponse 对象不为 null ,那么会取出 cacheResponse 对象的头信息,并且将其保存到 CacheStrategy 属性中。
Factory.get 方法内部会通过 getCandidate() 方法获取一个 CacheStrategy,因为关键代码就在 getCandidate() 中。
/**
* Returns a strategy to satisfy {@code request} using the a cached response {@code response}.
*/
public CacheStrategy get() {
CacheStrategy candidate = getCandidate();
if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
// We're forbidden from using the network and the cache is insufficient.
return new CacheStrategy(null, null);
}
return candidate;
}
- getCandidate() 负责去获取一个 CacheStrategy 对象。
/** Returns a strategy to use assuming the request can use the network. */
private CacheStrategy getCandidate() {
// No cached response.
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}
// Drop the cached response if it's missing a required handshake.
if (request.isHttps() && cacheResponse.handshake() == null) {
return new CacheStrategy(request, null);
}
// If this response shouldn't have been stored, it should never be used
// as a response source. This check should be redundant as long as the
// persistence store is well-behaved and the rules are constant.
if (!isCacheable(cacheResponse, request)) {
return new CacheStrategy(request, null);
}
CacheControl requestCaching = request.cacheControl();
if (requestCaching.noCache() || hasConditions(request)) {
return new CacheStrategy(request, null);
}
long ageMillis = cacheResponseAge();
long freshMillis = computeFreshnessLifetime();
if (requestCaching.maxAgeSeconds() != -1) {
freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
}
long minFreshMillis = 0;
if (requestCaching.minFreshSeconds() != -1) {
minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
}
long maxStaleMillis = 0;
CacheControl responseCaching = cacheResponse.cacheControl();
if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
}
if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
Response.Builder builder = cacheResponse.newBuilder();
if (ageMillis + minFreshMillis >= freshMillis) {
builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
}
long oneDayMillis = 24 * 60 * 60 * 1000L;
if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
}
return new CacheStrategy(null, builder.build());
}
Request.Builder conditionalRequestBuilder = request.newBuilder();
if (etag != null) {
conditionalRequestBuilder.header("If-None-Match", etag);
} else if (lastModified != null) {
conditionalRequestBuilder.header("If-Modified-Since", lastModifiedString);
} else if (servedDate != null) {
conditionalRequestBuilder.header("If-Modified-Since", servedDateString);
}
Request conditionalRequest = conditionalRequestBuilder.build();
return hasConditions(conditionalRequest)
? new CacheStrategy(conditionalRequest, cacheResponse)
: new CacheStrategy(conditionalRequest, null);
}
当内部的 networkRequest 不为 null,表示需要进行网络请求,若是 cacheResponse 不为表示可以使用缓存,这两个属性是通过 CacheStrategy 构造方法进行赋值的,调用者可以通过两个属性是否有值来决定是否要使用缓存还是直接进行网络请求。
cacheResponse 判空,为空,直接使用网络请求。
isCacheable 方法判断 cacheResponse 和 request 是否都支持缓存,只要一个不支持那么直接使用网络请求。
requestCaching 判断 noCache 和 判断请求头是否有 If-Modified-Since 和 If-None-Match
判断 cacheResponse 的过期时间(包括 maxStaleMillis 的判断),如果没有过期,则使用 cacheResponse。
cacheResponse 过期了,那么如果 cacheResponse 有 eTag/If-None-Match 属性则将其添加到请求头中。
- CacheStrateg取得对象结果后
通过cacheCandidate 、cacheResponse作出判断。如果缓存不为空,但是策略器得到的结果是不能用缓存,也就是 cacheResponse 为 null,这种情况就是将 cacheCandidate.body() 进行 close 操作。源码如下:
if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
}
- 被禁止使用网络,而缓存不足,则失败。
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();
}
- 当 networkrequest 和 cacheResponse 都不为空,那么进行网络请求。
Response networkResponse = null;
//进行网络请求。
networkResponse = chain.proceed(networkRequest);
//进行了网络请求,但是缓存策略器要求可以使用缓存,那么
// If we have a cache response too, then we're doing a conditional get.
if (cacheResponse != null) {
//validate 方法会校验该网络请求的响应码是否未 304
if (validate(cacheResponse, networkResponse)) {
//表示 validate 方法返回 true 表示可使用缓存 cacheResponse
Response response = cacheResponse.newBuilder()
.headers(combine(cacheResponse.headers(), networkResponse.headers()))
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
networkResponse.body().close();
// Update the cache after combining headers but before stripping the
// Content-Encoding header (as performed by initContentStream()).
cache.trackConditionalCacheHit();
cache.update(cacheResponse, response);
//return 就是缓存 response
return response;
} else {
closeQuietly(cacheResponse.body());
}
}