OKHttp 3.10源码解析(三):缓存机制

版权声明:本文为博主石月的原创文章,转载请注明出处 https://blog.csdn.net/liuxingrong666/article/details/84983541

本篇我们来讲解OKhttp的缓存处理,在网络请求中合理地利用本地缓存能有效减少网络开销,提高响应速度。HTTP报头也定义了很多控制缓存策略的域,我们先来认识一下HTTP的缓存策略。

一.HTTP缓存策略

HTTP缓存有多种规则,根据是否需要向服务器发起请求来分类,我们将其分为两大类:强制缓存和对比缓存。

强制缓存就是服务器会返回一个资源的到期时间,下一次客户端请求时,如果请求时间小于到期时间,那么就直接使用缓存, 否则请求服务器。

对比缓存是不管我们是否使用缓存,都要跟服务器发生交互,下面我们会具体介绍到对比缓存的相关实现。

1.1 expires

在HTTP/1.0中为服务器返回的到期时间,在下一次请求时,如果请求时间小于这个到期时间,那么就直接使用缓存,否则重新请求,当然如果客户端时间和服务器时间有差异的话也会产生误差,所以在HTTP/1.1基本上不使用expires了,而是使用Cache-Control代替。

1.2 Cache-Control

Cache-Control的优先级比expires高,其中no-cache和no-store表示不缓存,max-age表示缓存时间, 单位秒, 比如max-age=31536000表示365天内再次请求这条数据时,就直接使用缓存。

1.3 Last-Modified / If-Modified-Since

这种缓存规则就是上面提到的对比缓存,Last-Modified是服务器返回的代表这条数据的最后一次修改时间,如图

客户端下次请求这条数据的时候,会在If-Modified-Since带上这个最后修改时间

此时服务器会比对客户端发送的这个最后修改时间,如果和服务器的最后修改时间相同,代表资源没有被修改过,此时响应状态码为304,告诉客户端可以使用缓存。

1.4 ETag/If-None-Match(优先级高于Last-Modified/If-Modified-Since)

ETag是服务器返回给客户端的一个唯一标识(生成规则由服务器决定),可以通过ETag值来判断资源是否有被修改

当客户端再次请求时,可以在头部的If-None-Match字段加上这个标识

当服务器收到请求以后发现头部有If-None-Match,则将请求中的标识与被请求资源的标识进行对比,如果相同则返回304告知客户端缓存可用。

1.5  no-cache/no-store

不使用缓存

1.6 only-if-cached

只使用缓存

1.7 http缓存策略流程图

 

二.OKhttp的缓存策略

我们知道OKhttp的缓存工作是在拦截器CacheInterceptor中实现的,在CacheInterceptor有一个缓存策略类CacheStrategy很重要,所以我们先来讲解这个缓存策略类的具体实现

1.CacheStrategy缓存策略类详解

  CacheStrategy(Request networkRequest, Response cacheResponse) {
    this.networkRequest = networkRequest;
    this.cacheResponse = cacheResponse;
  }

这个是CacheStrategy的构造方法,其实OKhttp中会根据networkRequest 和CacheResponse的值的不同给出了不同的缓存策略,如下:

networkRequest cacheResponse result 结果
null null only-if-cached (表明不进行网络请求,且缓存不存在或者过期,返回504错误)
null non-null 不进行网络请求,直接返回缓存
non-null null 而且缓存不存在或者过去,直接访问网络
non-null non-null Header中包含ETag/Last-Modified标签,需要在满足条件下请求,需要访问网络

在缓存策略类中,我们使用工厂方法来构建其实例

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

其实这里主要获取缓存相应头中的各种http关于缓存策略的值,比如我们上面提到的Expires、Last-Modified、ETag等等。下面我们主要来看看Factory的get方法,获取缓存策略对象

 
    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请求,而缓存没有TLS握手,重新发起网络请求
      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-Modified-Since或If-None-Match,请求网络
      if (requestCaching.noCache() || hasConditions(request)) {
        return new CacheStrategy(request, null);
      }
      //获取缓存响应中的响应头的CacheControl 
      CacheControl responseCaching = cacheResponse.cacheControl();
      //直接使用缓存
      if (responseCaching.immutable()) {
        return new CacheStrategy(null, cacheResponse);
      }
      //获取响应年龄  
      long ageMillis = cacheResponseAge();
      //获取缓存保险时间
      long freshMillis = computeFreshnessLifetime();
      //如果请求里面也有最大持久时间,则选小的那个
      if (requestCaching.maxAgeSeconds() != -1) {
        freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
      }
      //响应的最小刷新时间,设置一个响应将会持续刷新的最小秒数,如果一个响应的minFresh过期
      //以后,那么缓存将不能被使用,需要重新请求网络  
      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());
      }
      //可以缓存  
      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);
    }

从上面逻辑中可以看到OKhttp缓存策略的实现,其中也可以看到http的缓存策略的实现,接下来我们就可以去看看缓存拦截器的实现

2.CacheInterceptor的详细解析

缓存拦截器的主要作用是,根据我们生成的缓存策略决定当前请求是否使用缓存还是请求网络,还有相关的响应保存或者更新操作

  @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.
    }

    // 如果网络请求,同时又没有符合条件的缓存,返回一个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());
      }
    }

    // 缓存不为null,此时使用缓存的对比策略
    if (cacheResponse != null) {
      //服务端返回503,说明缓存有效,将本地缓存和网络响应作合并
      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;
  }

通过上面的注释,我们也可以清楚缓存拦截器的逻辑了,下面我们再来看看缓存相关的关键类Cache类

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

从构造方法中我们也可以看到,Cache类中有持有一个DiskLruCache类,实际上缓存的增删改查最终也是由DiskLruCache类来实现。我们主要来看看Cache类的几个方法put、get、remove、update。

1.put()方法

  @Nullable CacheRequest put(Response response) {
    String requestMethod = response.request().method();
    //如果请求是"POST"、"PUT"、"DELETE"、"MOVE"的其中一个,则移除缓存,返回null不缓存
    if (HttpMethod.invalidatesCache(response.request().method())) {
      try {
        remove(response.request());
      } catch (IOException ignored) {
        // The cache cannot be written.
      }
      return null;
    }
    //如果不是GET请求,则不缓存,就是说只有get请求才进行缓存
    if (!requestMethod.equals("GET")) {
      return null;
    }

    if (HttpHeaders.hasVaryAll(response)) {
      return null;
    }

    //由response构建一个Entry对象,
    Entry entry = new Entry(response);
    DiskLruCache.Editor editor = null;
    try {
      //通过DiskLruCache写入缓存
      editor = cache.edit(key(response.request().url()));
      if (editor == null) {
        return null;
      }
      entry.writeTo(editor);
      return new CacheRequestImpl(editor);
    } catch (IOException e) {
      abortQuietly(editor);
      return null;
    }
  }

2.remove()方法

 void remove(Request request) throws IOException {
    cache.remove(key(request.url()));
  }

public static String key(HttpUrl url) {
    return ByteString.encodeUtf8(url.toString()).md5().hex();
  }

3.update()方法

  void update(Response cached, Response network) {
    Entry entry = new Entry(network);
    DiskLruCache.Snapshot snapshot = ((CacheResponseBody) cached.body()).snapshot;
    DiskLruCache.Editor editor = null;
    try {
      editor = snapshot.edit(); // Returns null if snapshot is not current.
      if (editor != null) {
        entry.writeTo(editor);
        editor.commit();
      }
    } catch (IOException e) {
      abortQuietly(editor);
    }
  }

4.get()方法

  @Nullable Response get(Request request) {
    String key = key(request.url());
    DiskLruCache.Snapshot snapshot;
    Entry entry;
    try {
      snapshot = cache.get(key);
      if (snapshot == null) {
        return null;
      }
    } catch (IOException e) {
      // Give up because the cache cannot be read.
      return null;
    }

    try {
      entry = new Entry(snapshot.getSource(ENTRY_METADATA));
    } catch (IOException e) {
      Util.closeQuietly(snapshot);
      return null;
    }

    Response response = entry.response(snapshot);

    if (!entry.matches(request, response)) {
      Util.closeQuietly(response.body());
      return null;
    }

    return response;
  }

至于DiskLruCache的缓存机制,本篇文章暂且不去研究了,有时间我们再专门开篇博客去详解。

OKhttp的缓存机制就到此结束,下一篇我们讲解连接池相关的知识。

 

猜你喜欢

转载自blog.csdn.net/liuxingrong666/article/details/84983541
今日推荐