okhttp 缓存分析

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


一、http 缓存

okhttp 的缓存实现是基于 http 协议的缓存机制,所以先看一下 http 的缓存,http 缓存根据是否向服务器请求数据分为强制缓存和协商缓存通过不同的 http 首部字段来约束

1. 强制缓存

强制缓存整体流程比较简单,就是在第一次访问服务器取到数据之后,在过期时间之内不会再去重复请求。实现这个流程的核心就是如何知道当前时间是否超过了过期时间。强制缓存的过期时间通过第一次访问服务器时返回的响应头获取。在 http 1.0 和 http 1.1 版本中通过不同的响应头字段实现

在 http 1.0 版本中,强制缓存通过 expires 响应头来实现。 expires 字段将资源的有效期告诉客户端,在 expires 指定的时间之前请求都使用缓存,在指定的时间之后重新请求服务器获取资源

在 http 1.1 版本中,强制缓存通过 Cache-Control 响应头来实现。Cache-Control 拥有多个指令

Cache-Control 缓存请求指令

指令参数说明
no-cache强制向原服务器再次验证
no-store不缓存请求或响应的任何内容
max-age = [秒]必需如果缓存时间小于指定的时间就使用缓存
max-stale = [秒]可省略即使缓存过期但如果还在 max-stale 指定的时间内仍使用缓存
min-fresh必需返回指定时间内不会过期的缓存
only-if-cached-仅使用缓存如果没有缓存返回状态码 504

Cache-Control 缓存响应指令

指令参数说明
public资源将被客户端和代理服务器缓存
private可省略资源仅被客户端缓存, 代理服务器不缓存
no-cached可省略缓存前必须先确认其有效性
no-store]不缓存请求或响应的任何内容
max-age = [秒]必需如果缓存时间小于指定的时间就使用缓存

2. 协商缓存

协商缓存与强制缓存的不同之处在于,协商缓存每次读取数据时都需要跟服务器通信,确保缓存有效性。在第一次请求服务器时,服务器会返回资源,并且返回一个资源的缓存标识。当第二次请求资源时,客户端会首先将缓存标识发送给服务器,服务器拿到标识后判断标识是否匹配,如果不匹配,表示资源有更新,服务器会将新数据和新的缓存标识一起返回到客户端;如果缓存标识匹配,表示资源没有更新,并且返回 304 状态码,客户端使用缓存。协商缓存使用 Last-Modified/If-Modified-Since 和 ETag/If-None-Match(优先级高于Last-Modified/If-Modified-Since) 实现

Last-Modified/If-Modified-Since:服务器在返回响应时在响应头里返回 Last-Modified 资源的最后修改时间,客户端再次请求服务器时在请求头里通过 If-Modified-Since 字段把时间带过去
服务器对这个时间进行对比如果最后的修改时间大于这个时间则响应整个内容,状态码为 200
否则响应码为 304 告诉客户端使用缓存

ETag/If-None-Match:服务器在返回响应时在响应头里返回资源的唯一标志 Etag,客户端再次请求服务器时在请求头里添加这个唯一标识,服务器对比这个资源标识不一样表示资源更新了返回资源,否则返回 304 告诉客户端使用缓存

二、okhttp 缓存

OkHttp 版本 3.13.1

ohttp 缓存主要涉及的类有以下几个

  • CacheInterceptor:缓存拦截器,由该类触发缓存的所有操作,算是缓存的入口类
  • CacheControl:对上述 Cache-Control 首部指令的封装,将首部字段解析为 CacheControl 对象
  • CacheStrategy:缓存策略类。根据请求头和缓存响应头决定缓存是否可用,具体的代码判断
  • Cache:缓存存储类,内部使用 DiskLruCache 将缓存写入到磁盘中,通过创建 OkHttpClient 对象时传入,默认为空不使用缓存
  • InternalCache:CacheInterceptor 用于操作缓存的对象,它实际上是将所有操作都转给了 Cache 对象
  • DiskLruCache:真正通过 lru 算法将缓存存储到磁盘上的类
public final class CacheStrategy {
  // 要使用的网络请求,如果此值为 null 表示不发送网络请求
  public final @Nullable Request networkRequest;

  // 可直接返回的缓存/需要向服务器验证有效性的缓存,如果此值为 null 表示不使用缓存
  public final @Nullable Response cacheResponse;

CacheStrategy 中有两个字段分别表示网络请求和缓存响应,CacheInterceptor 中通过请求和缓存创建 CacheStrategy 对象,再通过这两个值决定后续的逻辑

public final class CacheInterceptor implements Interceptor {
  final @Nullable InternalCache cache;

  public CacheInterceptor(@Nullable 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 对象
    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    Request networkRequest = strategy.networkRequest;
    Response cacheResponse = strategy.cacheResponse;

CacheStrategy 对象通过内部类 Factory 的 get 方法获得,先看一下 Factory 的构造方法

    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);
          }
        }
      }
    }

构造方法里做的事情就是根据缓存对象的响应头对变量赋值,待会创建 CacheStrategy 对象的时候需要

    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;
    }
    
private CacheStrategy getCandidate() {
      // 如果没有缓存则使用网络请求
      if (cacheResponse == null) {
        return new CacheStrategy(request, null);
      }

      // 如果是 https 请求但是没有握手信息则使用网络请求
      if (request.isHttps() && cacheResponse.handshake() == null) {
        return new CacheStrategy(request, null);
      }

	  // 如果请求或者响应头标记不使用缓存则使用网络请求
      if (!isCacheable(cacheResponse, request)) {
        return new CacheStrategy(request, null);
      }

	  // 请求头的 cache-control
      CacheControl requestCaching = request.cacheControl();
      // 如果请求头标记需要验证或者需要协商则使用网络请求
      if (requestCaching.noCache() || hasConditions(request)) {
        return new CacheStrategy(request, null);
      }

	  // 响应头的 cache-control
      CacheControl responseCaching = cacheResponse.cacheControl();

	  // 响应从创建到现在为止的存活时间
      long ageMillis = cacheResponseAge();
      long freshMillis = computeFreshnessLifetime();
	
	  // 取请求和响应中两者的小值
      if (requestCaching.maxAgeSeconds() != -1) {
        freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
      }
	 
	  // 请求设置的 min-refresh:即客户要求返回的响应在该时间内应该是新鲜的
      long minFreshMillis = 0;
      if (requestCaching.minFreshSeconds() != -1) {
        minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
      }

      long maxStaleMillis = 0;
      // 服务器不是必须验证并且设置了可接受的最大过期时间
      if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
        maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
      }

	  // !responseCaching.noCache() 表示是强制缓存
	  // 如果缓存的存活时间 + 客户端要求的最小有效时间 < 缓存有效时间 + 可接受的过期时间
	  // 则使用缓存
      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());
      }

      // 以下是将构造方法中解析的响应头添加到网络请求中
      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);
    }

CacheStrategy 的创建就是通过很多判断对网络请求 networkRequest 和缓存响应 cacheResponse 赋值,最后看一下 CacheInterceptor 中后续的处理逻辑

    // 如果不使用网络请求并且缓存响应也是空返回 504 的响应码
    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 (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 (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)) {
        // 写到缓存里这里是通过 DiskLruCache 来实现的
        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;

总结

okhttp 缓存的实现就是依据 http 协议解析请求响应头得到 CacheControl 对象再通过里面的属性判断构造了 CacheStrategy 对象,对 CacheStrategy 里的两个变量 networkRequest 和 cacheResponse 赋值来决定后续的逻辑是返回还是继续网络请求

参考与感谢

《图解HTTP》
OKHttp源码解析(六)–中阶之缓存基础
OkHttp 缓存
10分钟彻底搞懂Http的强制缓存和协商缓存

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值