OKHTTP拦截器缓存策略CacheInterceptor的简单分析

OKHTTP异步和同步请求简单分析
OKHTTP拦截器缓存策略CacheInterceptor的简单分析
OKHTTP拦截器ConnectInterceptor的简单分析
OKHTTP拦截器CallServerInterceptor的简单分析
OKHTTP拦截器BridgeInterceptor的简单分析
OKHTTP拦截器RetryAndFollowUpInterceptor的简单分析
OKHTTP结合官网示例分析两种自定义拦截器的区别

为什么需要缓存 Response?

  • 客户端缓存就是为了下次请求时节省请求时间,可以更快的展示数据。
  • OKHTTP 支持缓存的功能

HTTP 中几个常见的缓存相关的头信息

  • Expire 一般会放在响应头中,表示过期时间
    Expires: Thu, 12 Jan 2017 11:01:33 GMT
  • Cache-Control 表示缓存的时间 max-age = 60 表示可以缓存 60s
  • e-Tag 表示服务器返回的一个资源标识,下次客户端请求时将该值作为 key 为 If-None-Match 的值传给服务器判断,如果ETag没改变,则返回状态304。
  • Last-Modified 在浏览器第一次请求某一个URL时,服务器端的返回状态会是200,内容是你请求的资源,同时有一个Last-Modified的属性标记此文件在服务期端最后被修改的时间,格式类似这样:Last-Modified:Tue, 24 Feb 2009 08:01:04 GMT,第二次请求浏览器会向服务器传送If-Modified-Since,询问该时间之后文件是否有被修改过,如果资源没有变化,则自动返回HTTP304状态码,内容为空,这样就节省了传输数据量。

示例代码

String url = "http://www.imooc.com/courseimg/s/cover005_s.jpg";

//配置缓存的路径,和缓存空间的大小
Cache cache = new Cache(new File("/Users/zeal/Desktop/temp"),10*10*1024);

OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .readTimeout(15, TimeUnit.SECONDS)
                .writeTimeout(15, TimeUnit.SECONDS)
                .connectTimeout(15, TimeUnit.SECONDS)
                //打开缓存
                .cache(cache)
                .build();

final Request request = new Request.Builder()
                .url(url)
                //request 请求单独配置缓存策略
                //noCache(): 就算是本地有缓存,也不会读缓存,直接访问服务器
                //noStore(): 不会缓存数据,直接访问服务器
                //onlyIfCached():只请求缓存中的数据,不靠谱
                .cacheControl(new CacheControl.Builder().build())
                .build();
Call call = okHttpClient.newCall(request);

Response response = call.execute();
//读取数据
response.body().string();

System.out.println("network response:"+response.networkResponse());
System.out.println("cache response:"+response.cacheResponse());

//在创建 cache 开始计算
System.out.println("cache hitCount:"+cache.hitCount());//使用缓存的次数
System.out.println("cache networkCount:"+cache.networkCount());//使用网络请求的次数
System.out.println("cache requestCount:"+cache.requestCount());//请求的次数


//第一次的运行结果(没有使用缓存)
network response:Response{protocol=http/1.1, code=200, message=OK, url=http://www.imooc.com/courseimg/s/cover005_s.jpg}
cache response:null
cache hitCount:0
cache networkCount:1
cache requestCount:1
//第二次的运行结果(使用了缓存)
network response:null
cache response:Response{protocol=http/1.1, code=200, message=OK, url=http://www.imooc.com/courseimg/s/cover005_s.jpg}
cache hitCount:1
cache networkCount:0
cache requestCount:1

OKHTTP 的缓存原理?

  • 底层使用的是 DiskLruCache 缓存机制,这一点可以从 Cache 的构造中可以验证。
Cache(File directory, long maxSize, FileSystem fileSystem) {
     this.cache = DiskLruCache.create(fileSystem, directory, VERSION, ENTRY_COUNT, maxSize);
}

《OKHTTP拦截器缓存策略CacheInterceptor的简单分析》 缓存文件.png

050ddcd579f740670cf782629b66eb92.0
//缓存响应的头部信息
http://www.qq.com/
GET
1
Accept-Encoding: gzip
HTTP/1.1 200 OK
14
Server: squid/3.5.20
Date: Sun, 02 Jul 2017 02:54:01 GMT
Content-Type: text/html; charset=GB2312
Transfer-Encoding: chunked
Connection: keep-alive
Vary: Accept-Encoding
Expires: Sun, 02 Jul 2017 02:55:01 GMT
Cache-Control: max-age=60
Vary: Accept-Encoding
Content-Encoding: gzip
Vary: Accept-Encoding
X-Cache: HIT from nanjing.qq.com
OkHttp-Sent-Millis: 1498964041246
OkHttp-Received-Millis: 1498964041330


050ddcd579f740670cf782629b66eb92.1
该文件缓存的内容是请求体,都是经过编码的,所以就不贴出来了。
  • 缓存的切入点 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(EMPTY_BODY)
          .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 (validate(cacheResponse, networkResponse)) {
        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;
      } else {
        closeQuietly(cacheResponse.body());
      }
    }

    Response response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();

    if (HttpHeaders.hasBody(response)) {
      CacheRequest cacheRequest = maybeCache(response, networkResponse.request(), cache);
      response = cacheWritingResponse(cacheRequest, response);
    }

    return response;
  }
  • 从本地中寻找是否有缓存?

cache 就是在 OkHttpClient.cache(cache) 配置的对象,该对象内部是使用 DiskLruCache 实现的。

Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;

CacheStrategy

它是一个策略器,负责判断是使用缓存还是请求网络获取新的数据。内部有两个属性:networkRequest和cacheResponse,在 CacheStrategy 内部会对这个两个属性在特定的情况赋值。

  • networkRequest:若是不为 null ,表示需要进行网络请求
/** The request to send on the network, or null if this call doesn't use the network. */
  public final Request networkRequest;
  • cacheResponse:若是不为 null ,表示可以使用本地缓存
/** The cached response to return or validate; or null if this call doesn't use a cache. */
  public final Response cacheResponse;

得到一个 CacheStrategy 策略器

cacheCandidate它表示的是从缓存中取出的 Response 对象,有可能为null(在缓存为空的时候),在 new CacheStrategy.Factory 内部如果 cacheCandidate 对象不为 null ,那么会取出 cacheCandidate 的头信息,并且将其保存到 CacheStrategy 属性中。

CacheStrategy strategy = new CacheStrategy.Factory(now, 
chain.request(), cacheCandidate).get();
public Factory(long nowMillis, Request request, Response cacheResponse) {
  this.nowMillis = nowMillis;
  this.request = request;
  this.cacheResponse = 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);
      }
    }
  }
}
  • get() 方法获取一个 CacheStrategy 对象。

在 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 对象。当内部的 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 属性则将其添加到请求头中。
/** 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());
  }

  // Find a condition to add to the request. If the condition is satisfied, the response body
// will not be transmitted.
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;
} else {
  return new CacheStrategy(request, null); // No condition! Make a regular request.
}
Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();
Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);
Request conditionalRequest = request.newBuilder()
    .headers(conditionalRequestHeaders.build())
    .build();
return new CacheStrategy(conditionalRequest, cacheResponse);

策略器得出结果之后

  • 如果缓存不为空,但是策略器得到的结果是不能用缓存,也就是 cacheResponse 为 null,这种情况就是将 cacheCandidate.body() 进行 close 操作。
if (cacheCandidate != null && cacheResponse == null) {
      closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
    }
  • networkRequest 为空,表示不能进行网络请求,但是 cacheResponse 不为空,可以使用缓存中的 cacheResponse。
   // If we don't need the network, we're done.
    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .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());
      }
    }
  • invalite

校验是使用缓存中的 response 还是使用网络请求的 response
当返回 true 表示可以使用 缓存中的 response 当返回 false 表示需要使用网络请求的 response。

/**
 * Returns true if {@code cached} should be used; false if {@code network} response should be
 * used.
 */
private static boolean validate(Response cached, Response network) {
  //304 表示资源没有发生改变,服务器要求客户端继续使用缓存
  if (network.code() == HTTP_NOT_MODIFIED) return true;
  // The HTTP spec says that if the network's response is older than our
  // cached response, we may return the cache's response. Like Chrome (but
  // unlike Firefox), this client prefers to return the newer response.
  Date lastModified = cached.headers().getDate("Last-Modified");
  if (lastModified != null) {
    Date networkLastModified = network.headers().getDate("Last-Modified");
    //在缓存范围内,因此可以使用缓存
    if (networkLastModified != null
        && networkLastModified.getTime() < lastModified.getTime()) {
      return true;
    }
  }
  //表示不可以使用缓存
  return false;
}
  • 使用网络请求回来的 networkResponse

当缓存 cacheResponse 不可用时或者为空那就直接使用网络请求回来的 networkResponse。

Response response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();

缓存 response

if (HttpHeaders.hasBody(response)) {
      CacheRequest cacheRequest = maybeCache(response, networkResponse.request(), cache);
      response = cacheWritingResponse(cacheRequest, response);
    }
  • maybeCache

CacheStrategy.isCacheable 通过该方法判断是否支持缓存。
HttpMethod.invalidatesCache 通过该方法判断该请求是否为 GET 请求。

private CacheRequest maybeCache(Response userResponse, Request networkRequest,
    InternalCache responseCache) throws IOException {
  if (responseCache == null) return null;
  // Should we cache this response for this request?
  if (!CacheStrategy.isCacheable(userResponse, networkRequest)) {
    if (HttpMethod.invalidatesCache(networkRequest.method())) {
      try {
        responseCache.remove(networkRequest);
      } catch (IOException ignored) {
        // The cache cannot be written.
      }
    }
    return null;
  }
  // Offer this request to the cache.
  return responseCache.put(userResponse);
}
  • responseCache.put(userResponse);
    • 通过 DiskLruCache 将响应头信息写入到磁盘中。
      entry.writeTo(editor);
    • 将响应体写入到磁盘中。new CacheRequestImpl(editor)

该方法是 Cache 中的方法,负责将 userResponse 缓存到本地。

private CacheRequest put(Response response) {
  String requestMethod = response.request().method();
  if (HttpMethod.invalidatesCache(response.request().method())) {
    try {
      remove(response.request());
    } catch (IOException ignored) {
      // The cache cannot be written.
    }
    return null;
  }
  //OKHTTP 只支持 GET 请求的缓存
  if (!requestMethod.equals("GET")) {
    // Don't cache non-GET responses. We're technically allowed to cache
    // HEAD requests and some POST requests, but the complexity of doing
    // so is high and the benefit is low.
    return null;
  }
  if (HttpHeaders.hasVaryAll(response)) {
    return null;
  }
  Entry entry = new Entry(response);
  DiskLruCache.Editor editor = null;
  try {
    editor = cache.edit(urlToKey(response.request()));
    if (editor == null) {
      return null;
    }  
    //通过 DiskLruCache 将响应头信息写入到磁盘中。
    entry.writeTo(editor);
    //将响应体写入到磁盘中。
    return new CacheRequestImpl(editor);
  } catch (IOException e) {
    abortQuietly(editor);
    return null;
  }
}
  • 写入头信息
public void writeTo(DiskLruCache.Editor editor) throws IOException {
  BufferedSink sink = Okio.buffer(editor.newSink(ENTRY_METADATA));
  sink.writeUtf8(url)
      .writeByte('\n');
  sink.writeUtf8(requestMethod)
      .writeByte('\n');
  sink.writeDecimalLong(varyHeaders.size())
      .writeByte('\n');
  for (int i = 0, size = varyHeaders.size(); i < size; i++) {
    sink.writeUtf8(varyHeaders.name(i))
        .writeUtf8(": ")
        .writeUtf8(varyHeaders.value(i))
        .writeByte('\n');
  }
  sink.writeUtf8(new StatusLine(protocol, code, message).toString())
      .writeByte('\n');
  sink.writeDecimalLong(responseHeaders.size() + 2)
      .writeByte('\n');
  for (int i = 0, size = responseHeaders.size(); i < size; i++) {
    sink.writeUtf8(responseHeaders.name(i))
        .writeUtf8(": ")
        .writeUtf8(responseHeaders.value(i))
        .writeByte('\n');
  }
  sink.writeUtf8(SENT_MILLIS)
      .writeUtf8(": ")
      .writeDecimalLong(sentRequestMillis)
      .writeByte('\n');
  sink.writeUtf8(RECEIVED_MILLIS)
      .writeUtf8(": ")
      .writeDecimalLong(receivedResponseMillis)
      .writeByte('\n');
  if (isHttps()) {
    sink.writeByte('\n');
    sink.writeUtf8(handshake.cipherSuite().javaName())
        .writeByte('\n');
    writeCertList(sink, handshake.peerCertificates());
    writeCertList(sink, handshake.localCertificates());
    // The handshake’s TLS version is null on HttpsURLConnection and on older cached responses.
    if (handshake.tlsVersion() != null) {
      sink.writeUtf8(handshake.tlsVersion().javaName())
          .writeByte('\n');
    }
  }
  sink.close();
}
    原文作者:未见哥哥
    原文地址: https://www.jianshu.com/p/8d0bc05104ef
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞