Okhttp之CacheInterceptor详解

1,146 阅读20分钟

缓存知识

在开始Okhttp的缓存策略前,先了解HTTP权威指南中PC端和服务器之间的一些缓存知识 PC端缓存既可以是本地缓存,也可以是代理服务器上的缓存。但Okhttp中的缓存是指本地缓存,因为代理缓存也进行网络请求了,没有区分代理缓存还是服务器上的需求。 缓存命中和未命中:如果有缓存就叫缓存命中,同理没有缓存即只能通过源端服务器叫缓存未命中。 再验证:源端服务器的内容可能会发生变化,但缓存内容是一直不变,这时就需要再验证判断缓存是否过期,而再验证的操作不可能随时去检测,所以时机是客户端发起请求时。

再验证

再验证的两种方式:

1、Last-Modified和If-Modified-Since配合:获取到服务器响应后,客户端保存缓存时也保存了响应中的Last-Modified首部值,表示该内容最后的修改时间,如果缓存过期,会判断缓存中是否有Last-Modified首部,有则发起一个HEAD请求,携带一个If-Modified-Since首部,值是Last-Modified的值,服务器接收到会将现在内容的修改时间和传过来的比较,如果现在内容的修改时间比缓存中的大,说明修改了,即再验证未命中,再验证未命中结果上面说过,不然就是等于了就是再验证命中结果,客户端继续用缓存。

2、ETag和If-None-Match(优先级高于Last-Modified和If-Modified-Since)

获取到服务器响应后,客户端保存缓存时也保存了响应中的ETa首部值,ETa表示客户端当前资源在服务器的唯一标识(生成规则由服务器决定)。如果缓存过期,会判断缓存中是否有ETa首部,有则发起一个HEAD请求,携带一个If-None-Match首部,通过此字段通知服务器客户端缓存数据的唯一标识。服务器收到请求后发现有头部If-None-Match则与被请求的资源的唯一标识进行对比,不同则说明资源被改过,即再验证未命中结果,相同则说明资源没有被改动过,即再验证命中结果,客户端继续用缓存。

总结:只要请求时有If-Modified-Since或If-None-Match首部,就属于再验证,而请求方法为GET,其他方法不建议使用这个首部,因为本身Okhttp就只会缓存GET响应,所以其他请求方法没必要使用该首部,而且再验证也不能使用HEAD方法,因为要是再验证未命中需要返回新的body,而HEAD不返回body的。

那服务器对再验证请求有哪些响应:

1、再验证命中:源端服务器的内容没修改,即和缓存内容一样,响应是HTTP 304 NOT Modified,注意这时没有body的

2、再验证未命中:源端服务器的内容和缓存不同,服务器向客户端发送一条普通的、带有完整内容的HTTP 200 OK响应

3、源端服务器的内容被删除:服务器返回404 NOT Found响应,缓存也会被删除

再验证请求和原来请求的区别:就是有无If-Modified-Since或If-None-Match首部,表现不同的地方是再验证请求命中缓存时会返回304。

注意虽然有时添加了再验证首部,而且源端服务器的内容没改变,但有的源端服务器并没有对再验证首部做出304反应,而是200返回一样的内容。

判断缓存是否过期可以判断当时响应的Expires(绝对时间)或Cache-Control:max-age=xx(响应生成过去的时间),但实际上计算比这个复杂多了,具体看下面Okhttp中的处理。

注意我们在请求时如果Cache-Control:no cache,就会进行强制再验证,意思就是即使缓存没有过期,也会去再验证,如果再验证命中就使用缓存,如果再验证未命中就去返回源端服务器内容。

Okhttp的Cache-Control

Cache-Control 是缓存时最重要的首部。常见的取值有private、public、no-cache、max-age、no-store、默认是private。

针对Okhttp里 Cache-Control值的作用

但注意Cache-Control在请求和响应时都可以使用该首部,有些值能公用,有些值只有请求特有的,同理响应特有值

下面Cache-Control值的意义都是来自Okhttp的处理

请求时Cache-Control的值的意义:

no-cache:即使有缓存,即使缓存未过期,也会向服务器发起原始请求

no-store:缓存应该尽快被删除,避免敏感数据泄露,但Okhttp中并没有因为请求有no-store而去删除缓存,如果请求是no-store,向服务器发起原始请求。

max-age:s:如果本地或代理服务器的缓存时间大于s秒,就不会获取缓存,除非同时发送了max-stale:可以随意提供过期的文件,如果加max-stale:s,最长可以过期s秒。

only-if-cached(请求特有):不使用网络,缓存过期再验证或原始请求会返回504,未过期缓存返回缓存

max-stale(请求特有):接受过期时间,即缓存保质期是max-age加max-stale

min-fresh(请求特有):缓存的资源至少要保持指定时间的新鲜期,和max-stale意思相反,缓存保质期是max-age减min-fresh

比如第一次响应是是4月5日缓存, 且在4月12日过期(max-age=7),再次请求时max-age=10 days, max-stale=2days, min-fresh=3 days,那到底哪天算过期呢,

max-age=10对应4.5+10,即4.15过期

max-stale=2时到底是从4.12还是4.15开始呢,是以缓存中的过期时间算,即4.12算过期,加上max-stale的2是4.14算过期

min-fresh=3表示至少要留有3天的新鲜期, 缓存资源将在4月9日失效(12-3=9)。

这样看起来有三个日期,但因为总是采用最保守的缓存策略,,所以4.9发起请求时会再验证

响应时 Cache-Control的值的意义

no-cache:响应可以被缓存,但是每次请求如果满足再验证条件就原始请求添加再验证首部,否则不添加,再验证结果上面说过,成功就304,失败就200。这样看起来和请求时的no-cache不是一个意思,请求的no-cache会直接原始请求,而缓存的no-cache每次强制再验证

no-store: 响应不会被代理服务器缓存,代理服务器会转发no-store,但我觉得本地可以缓存,不然本地就没缓存,就没必要判断有没有no-store,Okhttp的确也是这样处理的,不管是put(第一次缓存)还是update(更新缓存),只要是GET请求就会去缓存,不管Cache-Control的值是什么,而再次请求时是不会用缓存的,是直接网络请求,获取到响应会去更新,但从来不用,所以no-store的响应就没必要缓存,虽然Okhttp是默认缓存的。

响应时的no-store和请求时的no-store一样,并不代表没有缓存,而是不会使用缓存,所以不会先进行再验证,直接去网络请求。而且只要任意一个有no-store就直接去网络请求,不使用缓存。

max-age:s:s秒之后缓存过期,那看起来和请求时是一样的意思,不过一个是响应时给定的过期时间,请求时的max-age是我们自定义过期时间,但两者取最小值。

must-revalidate(响应特有):可缓存但必须再向源服务器进行确认,这样看起来是直接每次原始请求,而事实上在Okhttp中的处理并不是这样的,他只有一个作用,那就是使请求的max-stale失效,但可能缓存未过期

注意:如果某个值是请求特有的,那么给响应添加是没有任何效果的,反之同理。

比如我响应中添加max-stale是没有用的,因为计算缓存时max-stale是从请求中查找的

CacheInterceptor

缓存策略总结

Okhttp中的缓存指本地缓存,策略实现就是靠本地缓存和Cache-Control结合实现的 服务器返回200那里,决定是否缓存不止受no-store影响,也有有些状态码本身不支持缓存,看iCacheStrategy的sCacheable(),而且只支持GET缓存,具体看Cache的put()

注意缓存的update()操作只有在返回304,才算update(),更新请求和响应时间,而再验证失败返回新的内容,这个是属于和第一次缓存一样,都是put操作。

上图共5种情况

情况1:无本地缓存

情况2:上面这四种情况,但注意第4点,可能有再验证首部,这样就可能返回304使用本地缓存

情况3:缓存过期,但没有Etag,LastModified,还是用原始请求

情况4:缓存过期,有Etag或LastModified,原始请求添加If-Modified-Since或If-None-Match

情况5:缓存未过期

缓存策略通过CacheStrategy类实现,最终的缓存策略是由CacheStrategy的networkRequest和cacheResponse决定

//原始请求,如果为null就不进行网络请求
public final @Nullable Request networkRequest;
//缓存,如果为null,就表示没有该缓存或者不使用缓存
public final @Nullable Response cacheResponse;
networkRequestcacheResponse场景和结果
nullnull情况1,2,3,4有only-if-cached,返回504空响应;情况5返回本地缓存
nullnot-null情况5返回本地缓存
not-nullnull情况1,2,3没有only-if-cached,返回原始请求响应并缓存
not-nullnot-null情况4 返回304使用本地缓存,返回200结果并缓存

注意事项

1、请求和响应中谨慎使用no-store,会导致无法使用本地缓存,除非请求手动添加再验证首部

2、请求和响应的no-cache效果不同,前者是直接原始请求,后者不会去计算缓存是否过期,可能会添加再验证首部

3、请求谨慎使用only-if-cached,这个是不使用网络,所以除非本地缓存未过期,其他情况(进行网络请求)都是504

源码简析

CacheInterceptor中根据当前Request和缓存cacheCandidate去实例化CacheStrategy,get()中决定networkRequest和cacheResponse的值,从而影响真正的缓存策略

public CacheStrategy get() {
  CacheStrategy candidate = getCandidate();
	//networkRequest!=null,可能是添加再验证的原始请求也有可能是默认的原始请求,
  //only-if-cached是不使用网络,所以冲突,结果是504空响应
  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;
}
private CacheStrategy getCandidate() {
  //无本地缓存,情况1
  if (cacheResponse == null) {
    return new CacheStrategy(request, null);
  }
  // Drop the cached response if it's missing a required handshake.
  //情况2.1:如果Https请求且丢失了握手,就将cacheResponse置为null,为啥?缓存body可能还在呀
  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.
  //情况2.2:通过请求和本地的缓存响应是否有no-store,有一个是就不使用缓存
  if (!isCacheable(cacheResponse, request)) {
    return new CacheStrategy(request, null);
  }
	//Cache-Control是no-catch或当前请求首部里有再验证的两个首部就不使用缓存
  //情况2.3和情况2.4:前者是强制不使用缓存,后者是缓存到期了去再验证
  CacheControl requestCaching = request.cacheControl();
  if (requestCaching.noCache() || hasConditions(request)) {
    return new CacheStrategy(request, null);
  }
	//计算缓存年龄,具体看下面,来自RFC 2616, 13.2.3 Age Calculations
  long ageMillis = cacheResponseAge();
  //获取缓存保鲜时间,具体看下面
  long freshMillis = computeFreshnessLifetime();

	//取请求的max-age时间和缓存响应新鲜值的最小值,因为两者都代表新鲜时间,request是我们要求的
  //response是服务器要求的,取小的保证缓存肯定新鲜
  if (requestCaching.maxAgeSeconds() != -1) {
    freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
  }

  long minFreshMillis = 0;
  //获取当前Request的Cache—Control的min-fresh的值,
  if (requestCaching.minFreshSeconds() != -1) {
    minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
  }
	
  long maxStaleMillis = 0;
  //缓存响应中有must-revalidate时请求时会每次去确认,所以要请求的max-stale有意义必须没有must-revalidate
  CacheControl responseCaching = cacheResponse.cacheControl();
  if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
    maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
  }
	//计算是否过期前提是缓存没有no-cache
  //没过期:ageMillis(响应报文的年龄)+min-fresh<最小的保鲜期+允许过期的时间
  //没过期就request为null,不进行网络请求
  //情况5
  if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
    Response.Builder builder = cacheResponse.newBuilder();
    //说明过期但还在max-stale内
    if (ageMillis + minFreshMillis >= freshMillis) {
      builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
    }
    long oneDayMillis = 24 * 60 * 60 * 1000L;
    //缓存超过一天且没有指定保鲜日期(即response没有max-age和expires),就给个警告
    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 {
  	//情况3:没有再验证条件就去网络请求
    return new CacheStrategy(request, null); // No condition! Make a regular request.
  }
		
  Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();
  Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);
	//情况4:原始请求添加再验证首部
  Request conditionalRequest = request.newBuilder()
      .headers(conditionalRequestHeaders.build())
      .build();
  return new CacheStrategy(conditionalRequest, cacheResponse);
}

isCacheable总结:下面提到的200等这些状态码是默认支持缓存,但最后真正是否能做为缓存还是得看当前Request和缓存的response的Cache-Control有没有no-store,有一个是no-sotre就不支持缓存。其他状态码不支持缓存,直接默认false。

public static boolean isCacheable(Response response, Request request) {
  // Always go to network for uncacheable response codes (RFC 7231 section 6.1),
  // This implementation doesn't support caching partial content.
  switch (response.code()) {
  	//200
    case HTTP_OK:
    //203,内容是代理服务器的缓存,但没有Authorization验证
    case HTTP_NOT_AUTHORITATIVE:
    //204,没有body
    case HTTP_NO_CONTENT:
    //300,重定向多个url
    case HTTP_MULT_CHOICE:
    //301,永久重定向
    case HTTP_MOVED_PERM:
    //404,无法找到url的资源
    case HTTP_NOT_FOUND:
    //405,方法不在url请求的方法内
    case HTTP_BAD_METHOD:
    //410,资源没了,类似404,但曾经拥有,服务器可以通知客户端
    case HTTP_GONE:
    //414,url太长
    case HTTP_REQ_TOO_LONG:
    //501,服务器遇到错误
    case HTTP_NOT_IMPLEMENTED:
    //308,永久重定向
    case StatusLine.HTTP_PERM_REDIRECT:
      // These codes can be cached unless headers forbid it.
      //上面这些虽然没返回真正内容,但是默认是允许缓存的
      break;
		//302,临时重定向
    case HTTP_MOVED_TEMP:
    //307,临时重定向,只能get重定向
    case StatusLine.HTTP_TEMP_REDIRECT:
      // These codes can only be cached with the right response headers.
      // http://tools.ietf.org/html/rfc7234#section-3
      // s-maxage is not checked because OkHttp is a private cache that should ignore s-maxage.
      //302或307的缓存必须没有Expires和max-age和非public和非private,否则直接false,不使用缓存
      //但是默认不是private的吗,注意这并不代表有缓存,只能说如果有缓存是默认是私有缓存
      //那意思是临时重定向的缓存不能拿来用?
      if (response.header("Expires") != null
          || response.cacheControl().maxAgeSeconds() != -1
          || response.cacheControl().isPublic()
          || response.cacheControl().isPrivate()) {
        break;
      }
      // Fall-through.

    default:
      // All other codes cannot be cached.
      return false;
  }

  // A 'no-store' directive on request or response prevents the response from being cached.
  //缓存有noStore和请求有noStore有一个满足就返回false,说明no-store就是不用缓存
  return !response.cacheControl().noStore() && !request.cacheControl().noStore();
}

注意:要实现缓存,必须client调用cache()或setInternalCache(),前者优先级高,而且前者InternalCache接口在Cache中有默认实现,该接口是CacheInterceptor和Cache的增删改查的连接器;如果是setInternalCache(),就必须自己实现InternalCache接口的所有方法,即对缓存的增删改查。如果两者都没设置,cache(InternalCache)就会为null,就没有缓存功能了。 如果是设置cache(),那么InternalCache在Cache中有个默认实现,通过internalCache回调Cache中的增删改查方法,代码就不贴出了。

现在再来看下CacheInterceptor

public CacheInterceptor(InternalCache cache) {
  this.cache = cache;
}
@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;
	//统计策略,如果networkRequest!=null,说明网络请求,Cache的networkCount++,
  //同理cacheResponse!=null对应Cache的hitCount++
  if (cache != null) {
    cache.trackResponse(strategy);
  }
	//如果cacheResponse为null,即不使用缓存,就关闭缓存cacheCandidate
  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 {
  	//执行到这里networkRequest不为null,但cacheResponse不一定为null,null说明不使用缓存
    //不为null是缓存过期但添加再验证
    //网络请求了
    networkResponse = chain.proceed(networkRequest);
  } finally {
    // If we're crashing on I/O or otherwise, don't leak the cache body.
    //响应没有body,但不使用缓存,就关闭缓存
    if (networkResponse == null && cacheCandidate != null) {
      closeQuietly(cacheCandidate.body());
    }
  }

  // If we have a cache response too, then we're doing a conditional get.
  //cacheResponse不为null说明是有再验证首部的原始请求
  if (cacheResponse != null) {
  	//状态码是304说明缓存没被改变,所以还是用cacheResponse
    if (networkResponse.code() == HTTP_NOT_MODIFIED) {
    //以缓存响应为基础构建一个新的response
      Response response = cacheResponse.newBuilder()
      		 //合并cacheResponse和networkResponse的首部
          .headers(combine(cacheResponse.headers(), networkResponse.headers()))
          //更新请求时间为再验证请求的时间
          .sentRequestAtMillis(networkResponse.sentRequestAtMillis())
          //更新响应时间为再验证响应时间
          .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
          //清空cacheResponse和networkResponse的body,不知道为什么清除,但缓存的body
          //已经通过newBuilder()已经传递给response了
          .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统计hitCount++
      cache.trackConditionalCacheHit();
      //更新缓存cacheResponse为现在的合并好的缓存response
      cache.update(cacheResponse, response);
      return response;
    } else {
    	//说明不是304,即缓存发生改变,关闭缓存
      closeQuietly(cacheResponse.body());
    }
  }
	 //执行到这说明没有缓存或缓存过期再验证未命中,返回新内容
  Response response = networkResponse.newBuilder()
      .cacheResponse(stripBody(cacheResponse))
      .networkResponse(stripBody(networkResponse))
      .build();
	//cache!=null,说明开启缓存的两种方法至少有一种
  if (cache != null) {
  	//前者是判断响应有无body,isCacheable也讲过,即如果响应和请求没有no-store,有的状态
    //码即使没有no-store也返回false,即不支持缓存,才去缓存
    if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
      // Offer this request to the cache.
      //第一次缓存写入本地
      CacheRequest cacheRequest = cache.put(response);
      return cacheWritingResponse(cacheRequest, response);
    }
		//判断请求如果是"POST"、"PATCH"、"PUT"、"DELETE"、"MOVE"中的任何一个则
    //调用DiskLruCache.remove(urlToKey(request));将这个请求从缓存中移除出去。
    if (HttpMethod.invalidatesCache(networkRequest.method())) {
      try {
        cache.remove(networkRequest);
      } catch (IOException ignored) {
        // The cache cannot be written.
      }
    }
  }

  return response;
}

计算缓存是否过期

计算缓存年龄

servedDate: 报文产生时间,对应响应的Date首部

receivedResponseMillis: 接收到响应,即构建Response的时间,为当前时间,如果是缓存再验证,不管成功与否,都更新receivedResponseMillis为当前时间;如果使用的是本地缓存,这个值还是上次网络请求的response的值。

sentRequestMillis:发起网络请求的时间,同上,只要进行网络请求,就会更新;用的本地缓存就还是上次的;504就是-1。

private long cacheResponseAge() {
  //receivedResponseMillis
  long apparentReceivedAge = servedDate != null
      ? Math.max(0, receivedResponseMillis - servedDate.getTime())
      : 0;
  long receivedAge = ageSeconds != -1
      ? Math.max(apparentReceivedAge, SECONDS.toMillis(ageSeconds))
      : apparentReceivedAge;
  long responseDuration = receivedResponseMillis - sentRequestMillis;
  long residentDuration = nowMillis - receivedResponseMillis;
  return receivedAge + responseDuration + residentDuration;
}

我们说过缓存是有本地缓存和代理服务器缓存的,缓存年龄就是从响应报文时间(servedDate)那刻开始计算,而且不管是代理服务器缓存还是直接从源端服务器取,响应报文时间是不变的,都是第一次产生响应的时间。

所以注意如果是代理服务器的缓存,servedDate要比发送请求时间早得多,如果是从源端服务器获取,源端服务器在接收到请求后产生servedDate。

在HTTP1.1时,如果从代理服务器获取到缓存,响应有个Age首部(响应年龄),=响应时的时间-响应报文产生时间,帮我们计算好了,但是HTTP1.1之前没有Age,只能自己计算,也是响应时的时间-响应报文产生时间,就是上面的apparentReceivedAge。所以为了适配所有情况,先计算apparentReceivedAge,如果有Age首部,说明是HTTP1.1取age和apparentReceivedAge的最大值,可能有时差误差,取大的。如果没有Age,那就用apparentReceivedAge

直接源端服务器获取的响应

获取的是代理服务器的缓存的年龄 response到nowMills表示获取响应后本地缓存的年龄

不管哪种方式,代码中计算缓存年龄总是receivedAge + responseDuration + residentDuration

主要serverDate到nowMills这段时间占大头

不过这样算的话,对比图的话,不知道为什么,都重复多算了一段时间。。。

缓存保鲜时间

缓存保鲜时间,优先是缓存响应的max-age指定的时间,没有的话是expires(绝对日期)减报文产生日期,

如果两种都没设置,那么是(某次响应中返回的lastmodify时间-报文产生日期)/10,不太懂。

注意,如果computeFreshnessLifetime()返回0(即响应没有max-age或expires)会导致即使设置了请求的max-age还是0,所以在响应中修改max-age

private long computeFreshnessLifetime() {
  CacheControl responseCaching = cacheResponse.cacheControl();
  if (responseCaching.maxAgeSeconds() != -1) {
    return SECONDS.toMillis(responseCaching.maxAgeSeconds());
  } else if (expires != null) {
    long servedMillis = servedDate != null
        ? servedDate.getTime()
        : receivedResponseMillis;
    long delta = expires.getTime() - servedMillis;
    return delta > 0 ? delta : 0;
  } else if (lastModified != null
      && cacheResponse.request().url().query() == null) {
    // As recommended by the HTTP RFC and implemented in Firefox, the
    // max age of a document should be defaulted to 10% of the
    // document's age at the time it was served. Default expiration
    // dates aren't used for URIs containing a query.
    long servedMillis = servedDate != null
        ? servedDate.getTime()
        : sentRequestMillis;
    long delta = servedMillis - lastModified.getTime();
    return delta > 0 ? (delta / 10) : 0;
  }
  return 0;
}
if (requestCaching.maxAgeSeconds() != -1) {
  freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
}
计算缓存是否过期
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());
}

注意响应不能有no-cache,否则直接再验证,不管缓存是否过期

ageMillis + minFreshMillis < freshMillis + maxStaleMillis

缓存年龄+min-fresh<缓存保鲜时间+max-stale

缓存年龄上面我们计算过,最好不要去修改。304时更新响应时间和请求时间,相当于更新了ageMillis,但其实感觉影响也不大,now-serverData占大头,serverData没变。

我们可以改变请求时的min-fresh来提前使缓存过期,没必要使用负值,那就是max-stale的意思了,其实min-fresh也用到不多,多数是直接不使用缓存,min-fresh可以值很大,但还不如请求时no-cache或no-store,直接原始请求

CacheControl中的FORCE_NETWORK就是添加了no-cache,也可以自己添加addHead(Cache-Control,"no-cache")

public static final CacheControl FORCE_NETWORK = new Builder().noCache().build();
request=request.newBuilder()
                .cacheControl(CacheControl.FORCE_NETWORK)
                .build()

我们可以修改请求时的max-stale来延时缓存过期,注意must-revalidate会使max-stale失效,当我们只想用本地缓存时,使用only-if-cache,但如果有网络请求(缓存过期再验证的原始请求或原始请求)都会返回504,那就必须走情况5,让缓存不过期,给max-stale设置无限大,但注意要走到情况5,必须不能满足情况2,即不能有no-cache或no-store。CacheControl类来提供Cache-Control的值,其内部的FORCE_CACHE就是只使用本地缓存。

 public static final CacheControl FORCE_CACHE = new Builder()
      .onlyIfCached()
      .maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS)
      .build();

实战缓存

通过internalCache的trackConditionalCacheHit()和trackResponse()回调来判断到底执行了什么操作

requestCount++:只要执行到缓存拦截器就会累加,即只要发起请求,不管走不走网络请求就会累加,因为重试机制可能一次请求会多次增加

networkCount++:缓存策略的网络请求不为null,情况上面说过,可能是缓存过期原始请求添加再验证,也可能是直接原始请求,总之就是进行网络请求了

hitCount++:有两处,一是缓存策略的网络请求为null,缓存不为null,就是缓存未过期;二是304更新了缓存。

但是这两种情况是互斥的,一次请求不可能加2,即每次加1

而networkCount和hitCount也是互斥的,requestCount=networkCount+hitCount

再次请求,只要cache()里Cache实例不变,那么上面几个值是累加的,虽然拦截器都实例化是新的了。

需求1

没有网络时只使用本地缓存,有网络时90s内使用本地缓存,这个时间根据实际情况调整,要是数据后台频繁刷新那就调小点,反之大点

class MainActivity1 : AppCompatActivity() {

    val mCachePath = File(AppContext.sContext.externalCacheDir, "cache")
    val cache = Cache(mCachePath, 10 * 1024 * 1024)
    lateinit var client: OkHttpClient
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        //list可以重复元素,所以mCacheInterceptor执行了2次,但不影响
        val mCacheInterceptor=MyCacheInterceptor(cache)
        client = OkHttpClient.Builder().cookieJar(MyCookieJar())
                .cache(cache)
                .addInterceptor(mCacheInterceptor)
                .addNetworkInterceptor(mCacheInterceptor)
                .build()
        request_btn.setOnClickListener {
            response_tv.text = ""
            handle()
        }
        handle()
    }

    private fun handle() {
        client.newCall(getRequest()).enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                Log.d("sdfasfasfsf", "error=" + e.message)
            }

            @Throws(IOException::class)
            override fun onResponse(call: Call, response: Response) {
                Log.d("sdfasfasfsf", "responseCode=" + response.code())
                val res = response.body()!!.string()
                runOnUiThread { response_tv.text = res }
            }
        })
    }

    private fun getRequest() = Request.Builder()
            .url("https://www.wanandroid.com/lg/collect/list/0/json")
            .build()


}

class MyCacheInterceptor(val cache: Cache) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        var request = chain.request()
        if (!isNetworkAvailable()) {
            //无网络只使用本地没缓存,注意FORCE_CACHE是only-if-cache+很大的max-stale
            // 只使用only-if-cache会容易再验证请求导致504
            request = request.newBuilder()
                    .cacheControl(CacheControl.FORCE_CACHE)
                    .build()
        }
        val response = chain.proceed(request)
        Log.d("sdfasfasfsf", "request" + cache.requestCount())
        Log.d("sdfasfasfsf", "NetRequest" + cache.networkCount())
        Log.d("sdfasfasfsf", "hitCount" + cache.hitCount())
        //  if (!isNetworkAvailable()) {
        //这种无网络设置max-age毫无意义,因为无网络时本身FORCE_CACHE通过max-stale
        //使缓存不过期,再添加max-age多此一举
//            val maxAge = 0
//            return response.newBuilder()
//                    .removeHeader("Pragma")
//                    .header("Cache-Control", "max-age=$maxAge")
//                    .build()
        //} else {
        //如果请求的min-frresh和max-stale都不设置,那就是90s内保证缓存不过期,保证使用本地缓存
        //上面FORCE_CACHE是上次的,不影响下次请求request,都实例化新request了
        val maxAge = 90//90s
        return response.newBuilder()
                //HTTP/1.0 缓存可能没有实现 Cache-Control,并且也没有实现 Pragma: no-cache
                //不知道什么用,兼容提示作用?
                .removeHeader("Pragma")
                //不用担心响应有no-cache或no-store,因为是先移除Cache-Control的所有值再添加新的
                .header("Cache-Control", "max-age=$maxAge")
                .build()

        //   }
        return response
    }

    private fun isNetworkAvailable(): Boolean {
        var mConnectivityManager: ConnectivityManager = AppContext.getContext().getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
        val mNetworkInfo: NetworkInfo? = mConnectivityManager.getActiveNetworkInfo()
        if (mNetworkInfo != null)
            return mNetworkInfo?.isAvailable()
        return false
    }

}

class MyCookieJar : CookieJar {
    var mCookies = mutableListOf<Cookie>()
    override fun saveFromResponse(url: HttpUrl, cookies: MutableList<Cookie>) {
        mCookies.clear()
        mCookies = cookies
    }

    override fun loadForRequest(url: HttpUrl): MutableList<Cookie> {
        if (mCookies.isEmpty()) {
            val cookieUser = Cookie.Builder()
                    .name("loginUserName")
                    .value("xxx")
                    .domain("s")
                    .build()
            val cookiePassword = Cookie.Builder()
                    .name("loginUserPassword")
                    .value("xxx")
                    .domain("a")
                    .build()
            mCookies.add(cookieUser)
            mCookies.add(cookiePassword)
        }
        return mCookies
    }
}

代码中有些注释说过了,还需注意几点:

1、没有网络时只使用本地缓存时,那本地缓存都没有,那就只能由CacheInterceptor返回504了

2、设置max-age可以根据后台数据刷新时间调整,无网络时FORCE_CACHE已经设置max-stale很大就没必要设置max-age了。注意添加的是响应中的max-age,看过上面的缓存计算知道,有的时候响应没有max-age或expires,

请求的max-age就会失效,最终的max-age还是0。

3、如果想设置max-age必须添加addNetworkInterceptor(),因为addInterceptor()添加的拦截器获取的响应是CacheInterceptor处理后返回的,而进行网络请求后CacheInterceptor先是将响应缓存后(put或update)才返回,所以拦截器添加的max-age并没有添加到本地缓存中,下次读取本地缓存的max-age不是自己修改的。所以添加addNetworkInterceptor()后先将响应结果修改max-age,然后让CacheInterceptor保存或更新缓存。

4、上面例子的addInterceptor()和addNetworkInterceptor()用的是同一个类作为拦截器,其实应该各自一个更好,

addInterceptor专门处理请求前和最终返回结果

addNetworkInterceptor专门处理刚返回的响应

用同一个类,多执行了无意义代码,就比如max-age修改只在NetworkInterceptor中才有用。

分开写如下:

client = OkHttpClient.Builder().cookieJar(MyCookieJar())
        .cache(cache)
        .addInterceptor(RequestCacheInterceptor(cache))
        .addNetworkInterceptor(ResponseCacheInterceptor())
        .build()
class RequestCacheInterceptor(val cache: Cache) : Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {
        var request = chain.request()
        if (!isNetworkAvailable()) {
            request = request.newBuilder()
                    .cacheControl(CacheControl.FORCE_CACHE)
                    .build()
        }
        val response=chain.proceed(request)
        Log.d("sdfasfasfsf", "request" + cache.requestCount())
        Log.d("sdfasfasfsf", "NetRequest" + cache.networkCount())
        Log.d("sdfasfasfsf", "hitCount" + cache.hitCount())
        return response
    }
}
class ResponseCacheInterceptor: Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val response = chain.proceed(chain.request())
        val maxAge = 30//30s
        return response.newBuilder()
                .removeHeader("Pragma")
                .header("Cache-Control", "max-age=$maxAge")
                .build()
        return response
    }
}

第一次请求打印结果:

request1

NetRequest1

hitCount0

30s内第二次请求打印结果:

request2

NetRequest1

hitCount1

30s(缓存过期)请求打印结果:

request4

NetRequest3

hitCount1

的确是网络请求,但为什么加2呢,说明缓存拦截器被重复执行,而重复执行除了我们自己主动调用chain.proceed(request)以外的唯一原因是请求失败,RetryAndFollowUpInterceptor重试重新执行一遍拦截器。

可以dubug发现RetryAndFollowUpInterceptor的第一次返回IO异常,至于为什么一定要重试第二遍有待研究。

无网络时请求打印结果

request5

NetRequest3

hitCount2

的确使用了缓存

需求2:

每次都进行原始网络请求,那就不要设置cache()了,或者设置cache()后请求带no-cache或no-store或响应带no-store

如果想每次强制再验证,这和上面的每次进行原始网络请求还是不同的,强制再验证是有缓存功能的,虽然每次再验证,但是如果再验证命中返回304就使用缓存,可比原始网络请求需要传递body快多了,这种适合数据时刻保持最新,只要响应添加no-cache即可,这种是和max-age是冲突的,所以写法上只要将需求1里的max-age替换成no-cache即可。至于无网络只使用缓存可以根据情况是否删掉。

return response.newBuilder()
                //HTTP/1.0 缓存可能没有实现 Cache-Control,并且也没有实现 Pragma: no-cache
                //不知道什么用,兼容提示作用?
                .removeHeader("Pragma")
                .header("Cache-Control", "no-cache")
                .build()

需求3:

在1需求下,如果服务器返回异常,那么就去使用缓存。

思路:那肯定是在addInterceptor中操作,因为要再次去执行CacheInterceptor,这时为了使用本地缓存,那肯定是only-if-cache

这个没测试过

class RequestCacheInterceptor(val cache: Cache) : Interceptor {
    lateinit var response: Response
    override fun intercept(chain: Interceptor.Chain): Response {
        var request = chain.request()
        if (!isNetworkAvailable()) {
            request = request.newBuilder()
                    .cacheControl(CacheControl.FORCE_CACHE)
                    .build()
        }
        response=chain.proceed(request)
        //随便写的,根据实际情况调整
        if(response.code()==200){
            request = request.newBuilder()
                    .cacheControl(CacheControl.FORCE_CACHE)
                    .build()
            //如果本地缓存都没有,那就只能返回504了
            response=chain.proceed(request)
        }
        Log.d("sdfasfasfsf", "request" + cache.requestCount())
        Log.d("sdfasfasfsf", "NetRequest" + cache.networkCount())
        Log.d("sdfasfasfsf", "hitCount" + cache.hitCount())
        return response
    }
}
//不变
class ResponseCacheInterceptor: Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val response = chain.proceed(chain.request())
        val maxAge = 30//30s
        return response.newBuilder()
                .removeHeader("Pragma")
                .header("Cache-Control", "max-age=$maxAge")
                .build()
        return response
    }
}

假如按上面测试用200(异常情况跟着调整),也就是说返回200就去使用本地缓存,运行第一次就有缓存但过期:

request2

NetRequest1

hitCount1

刚开始网络请求NetRequest++,然后返回200再次请求但使用本地缓存,所以hitCount++。

总结

核心就是这张图