前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >网络请求框架OkHttp3全解系列 - (三)拦截器详解1:重试重定向、桥、缓存(重点)

网络请求框架OkHttp3全解系列 - (三)拦截器详解1:重试重定向、桥、缓存(重点)

作者头像
胡飞洋
发布2020-07-23 16:16:28
2.2K0
发布2020-07-23 16:16:28
举报
文章被收录于专栏:胡飞洋的Android进阶

在本系列的上一篇文章中,我们走读了一遍okhttp的源码,初步了解了这个强大的网络框架的基本执行流程。

不过,上一篇文章只能说是比较粗略地阅读了okhttp整个执行流程方面的源码,搞明白了okhttp的基本工作原理,但并没有去深入分析细节(事实上也不可能在一篇文章中深入分析每一处源码的细节)。那么本篇文章,我们对okhttp进行深入地分析,慢慢将okhttp中的各项功能进行全面掌握。

今天文章中的源码都建在上一篇源码分析的基础之上,还没有看过上一篇文章的朋友,建议先去阅读 网络请求框架OkHttp3全解系列 - (二)OkHttp的工作流程分析 。这篇中我们知道,网络请求的真正执行是通过拦截器链关联的各个拦截器进行处理,每个拦截器负责不同的功能,下面将详细分析每个拦截器,包括重要知识点——缓存、连接池。

先回顾下RealCall的getResponseWithInterceptorChain方法——拦截器的添加以及拦截器链的执行:

代码语言:javascript
复制
  Response getResponseWithInterceptorChain() throws IOException {
    // 创建一系列拦截器
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());
    interceptors.add(new RetryAndFollowUpInterceptor(client));
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    interceptors.add(new CacheInterceptor(client.internalCache()));
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));

    Interceptor.Chain chain = new RealInterceptorChain(interceptors, transmitter, null, 0,
        originalRequest, this, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());
    ...
      //拦截器链的执行
      Response response = chain.proceed(originalRequest);
      ...
      return response;
    ...
  }

RetryAndFollowUpInterceptor - 重试、重定向

如果请求创建时没有添加应用拦截器,那么第一个拦截器就是RetryAndFollowUpInterceptor,意为“重试和跟进拦截器”,作用是连接失败后进行重试、对请求结果跟进后进行重定向。直接看它的intercept方法:

代码语言:javascript
复制
  @Override public Response intercept(Chain chain) throws IOException {
    Request request = chain.request();
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Transmitter transmitter = realChain.transmitter();

    int followUpCount = 0;
    Response priorResponse = null;

    while (true) {
      //准备连接
      transmitter.prepareToConnect(request);

      if (transmitter.isCanceled()) {
        throw new IOException("Canceled");
      }

      Response response;
      boolean success = false;
      try {
        //继续执行下一个Interceptor
        response = realChain.proceed(request, transmitter, null);
        success = true;
      } catch (RouteException e) {
        // 连接路由异常,此时请求还未发送。尝试恢复~
        if (!recover(e.getLastConnectException(), transmitter, false, request)) {
          throw e.getFirstConnectException();
        }
        continue;
      } catch (IOException e) {
        // IO异常,请求可能已经发出。尝试恢复~
        boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
        if (!recover(e, transmitter, requestSendStarted, request)) throw e;
        continue;
      } finally {
        // 请求没成功,释放资源。
        if (!success) {
          transmitter.exchangeDoneDueToException();
        }
      }

      // 关联上一个response
      if (priorResponse != null) {
        response = response.newBuilder()
            .priorResponse(priorResponse.newBuilder()
                    .body(null)
                    .build())
            .build();
      }

      Exchange exchange = Internal.instance.exchange(response);
      Route route = exchange != null ? exchange.connection().route() : null;
      //跟进结果,主要作用是根据响应码处理请求,返回Request不为空时则进行重定向处理-拿到重定向的request
      Request followUp = followUpRequest(response, route);

      //followUp为空,不需要重试,直接返回
      if (followUp == null) {
        if (exchange != null && exchange.isDuplex()) {
          transmitter.timeoutEarlyExit();
        }
        return response;
      }

      //followUpBody.isOneShot,不需要重试,直接返回
      RequestBody followUpBody = followUp.body();
      if (followUpBody != null && followUpBody.isOneShot()) {
        return response;
      }

      closeQuietly(response.body());
      if (transmitter.hasExchange()) {
        exchange.detachWithViolence();
      }

      //最多重试20次
      if (++followUpCount > MAX_FOLLOW_UPS) {
        throw new ProtocolException("Too many follow-up requests: " + followUpCount);
      }
      //赋值,以便再次请求
      request = followUp;
      priorResponse = response;
    }
  }

使用while进行循环

先用transmitter.prepareToConnect(request)进行连接准备。transmitter在上一篇有提到,意为发射器,是应用层和网络层的桥梁,在进行 连接、真正发出请求和读取响应中起到很重要的作用。看下prepareToConnect方法:

代码语言:javascript
复制
  public void prepareToConnect(Request request) {
    if (this.request != null) {
      if (sameConnection(this.request.url(), request.url()) && exchangeFinder.hasRouteToTry()) {
        return; //已有相同连接
      }
      ...
    }

    this.request = request;
    //创建ExchangeFinder,目的是为获取连接做准备
    this.exchangeFinder = new ExchangeFinder(this, connectionPool, createAddress(request.url()),
        call, eventListener);
  }

主要是创建ExchangeFinder梳理赋值给transmitter.exchangeFinder。ExchangeFinder是交换查找器,作用是获取请求的连接。这里先了解下,后面会具体说明。

接着调用realChain.proceed 继续传递请求给下一个拦截器、从下一个拦截器获取原始结果。如果此过程发生了 连接路由异常 或 IO异常,就会调用recover判断是否进行重试恢复。看下recover方法:

代码语言:javascript
复制
  private boolean recover(IOException e, Transmitter transmitter,
      boolean requestSendStarted, Request userRequest) {
    // 应用层禁止重试,就不重试
    if (!client.retryOnConnectionFailure()) return false;

    // 不能再次发送请求,就不重试
    if (requestSendStarted && requestIsOneShot(e, userRequest)) return false;

    // 发生的异常是致命的,就不重试
    if (!isRecoverable(e, requestSendStarted)) return false;

    // 没有路由可以尝试,就不重试
    if (!transmitter.canRetry()) return false;

    return true;
  }

如果recover方法返回true,那么就会进入下一次循环,重新请求。

如果realChain.proceed没有发生异常,返回了结果response,就会使用followUpRequest方法跟进结果并构建重定向request。如果不用跟进处理(例如响应码是200),则返回null。看下followUpRequest方法:

代码语言:javascript
复制
  private Request followUpRequest(Response userResponse, @Nullable Route route) throws IOException {
  ...
    int responseCode = userResponse.code();
  ...
    switch (responseCode) {
      ...
      case HTTP_PERM_REDIRECT:
      case HTTP_TEMP_REDIRECT:
        if (!method.equals("GET") && !method.equals("HEAD")) {
          return null;
        }
      case HTTP_MULT_CHOICE:
      case HTTP_MOVED_PERM:
      case HTTP_MOVED_TEMP:
      case HTTP_SEE_OTHER:
        if (!client.followRedirects()) return null;

        String location = userResponse.header("Location");
        if (location == null) return null;
        HttpUrl url = userResponse.request().url().resolve(location);

        if (url == null) return null;
        boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme());
        if (!sameScheme && !client.followSslRedirects()) return null;

        Request.Builder requestBuilder = userResponse.request().newBuilder();
        if (HttpMethod.permitsRequestBody(method)) {
          final boolean maintainBody = HttpMethod.redirectsWithBody(method);
          if (HttpMethod.redirectsToGet(method)) {
            requestBuilder.method("GET", null);
          } else {
            RequestBody requestBody = maintainBody ? userResponse.request().body() : null;
            requestBuilder.method(method, requestBody);
          }
          if (!maintainBody) {
            requestBuilder.removeHeader("Transfer-Encoding");
            requestBuilder.removeHeader("Content-Length");
            requestBuilder.removeHeader("Content-Type");
          }
        }
        if (!sameConnection(userResponse.request().url(), url)) {
          requestBuilder.removeHeader("Authorization");
        }

        return requestBuilder.url(url).build();

      case HTTP_CLIENT_TIMEOUT:
        ...
      default:
        return null;
    }
  }

主要就是根据响应码判断如果需要重定向,就从响应中取出重定向的url并构建新的Request并返回出去。

往下看,还有个判断:最多重试20次。到这里,RetryAndFollowUpInterceptor就讲完了,还是比较简单的,主要就是连接失败的重试、跟进处理重定向。

BridgeInterceptor - 桥接拦截器

接着是 BridgeInterceptor ,意为 桥拦截器,相当于 在 请求发起端 和 网络执行端 架起一座桥,把应用层发出的请求 变为 网络层认识的请求,把网络层执行后的响应 变为 应用层便于应用层使用的结果。 看代码:

代码语言:javascript
复制
//桥拦截器
public final class BridgeInterceptor implements Interceptor {

  //Cookie管理器,初始化OkhttpClient时创建的,默认是CookieJar.NO_COOKIES
  private final CookieJar cookieJar;

  public BridgeInterceptor(CookieJar cookieJar) {
    this.cookieJar = cookieJar;
  }

  @Override public Response intercept(Chain chain) throws IOException {
    Request userRequest = chain.request();
    Request.Builder requestBuilder = userRequest.newBuilder();

    RequestBody body = userRequest.body();
    if (body != null) {
      MediaType contentType = body.contentType();
      if (contentType != null) {
        requestBuilder.header("Content-Type", contentType.toString());
      }

      long contentLength = body.contentLength();
      //如果已经知道body的长度,就添加头部"Content-Length"
      if (contentLength != -1) {
        requestBuilder.header("Content-Length", Long.toString(contentLength));
        requestBuilder.removeHeader("Transfer-Encoding");
      } else {
      //如果不知道body的长度,就添加头部"Transfer-Encoding",代表这个报文采用了分块编码。这时,报文中的实体需要改为用一系列分块来传输。具体理解请参考:[HTTP Transfer-Encoding介绍](https://blog.csdn.net/Dancen/article/details/89957486)
        requestBuilder.header("Transfer-Encoding", "chunked");
        requestBuilder.removeHeader("Content-Length");
      }
    }

    if (userRequest.header("Host") == null) {
      requestBuilder.header("Host", hostHeader(userRequest.url(), false));
    }

    if (userRequest.header("Connection") == null) {
      requestBuilder.header("Connection", "Keep-Alive");
    }

    //"Accept-Encoding: gzip",表示接受:返回gzip编码压缩的数据
    // 如果我们手动添加了 "Accept-Encoding: gzip" ,那么下面的if不会进入,transparentGzip是false,就需要我们自己处理数据解压。
    //如果 没有 手动添加"Accept-Encoding: gzip" ,transparentGzip是true,同时会自动添加,而且后面也会自动处理解压。
    boolean transparentGzip = false;
    if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
      transparentGzip = true;
      requestBuilder.header("Accept-Encoding", "gzip");
    }
    //从cookieJar中获取cookie,添加到header
    List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
    if (!cookies.isEmpty()) {
      requestBuilder.header("Cookie", cookieHeader(cookies));
    }
    //"User-Agent"一般需要作为公共header外部统一添加
    if (userRequest.header("User-Agent") == null) {
      requestBuilder.header("User-Agent", Version.userAgent());
    }

    //处理请求
    Response networkResponse = chain.proceed(requestBuilder.build());
    //从networkResponse中获取 header "Set-Cookie" 存入cookieJar
    HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());

    Response.Builder responseBuilder = networkResponse.newBuilder()
        .request(userRequest);

    //如果我们没有手动添加"Accept-Encoding: gzip",这里会创建 能自动解压的responseBody--GzipSource
    if (transparentGzip
        && "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
        && HttpHeaders.hasBody(networkResponse)) {
      GzipSource responseBody = new GzipSource(networkResponse.body().source());
      Headers strippedHeaders = networkResponse.headers().newBuilder()
          .removeAll("Content-Encoding")
          .removeAll("Content-Length")
          .build();
      responseBuilder.headers(strippedHeaders);
      String contentType = networkResponse.header("Content-Type");
      responseBuilder.body(new RealResponseBody(contentType, -1L, Okio.buffer(responseBody)));
    }
    //然后构建 新的response返回出去
    return responseBuilder.build();
  }

  private String cookieHeader(List<Cookie> cookies) {
    StringBuilder cookieHeader = new StringBuilder();
    for (int i = 0, size = cookies.size(); i < size; i++) {
      if (i > 0) {
        cookieHeader.append("; ");
      }
      Cookie cookie = cookies.get(i);
      cookieHeader.append(cookie.name()).append('=').append(cookie.value());
    }
    return cookieHeader.toString();
  }
}

首先,chain.proceed() 执行前,对 请求添加了header:"Content-Type"、"Content-Length" 或 "Transfer-Encoding"、"Host"、"Connection"、"Accept-Encoding"、"Cookie"、"User-Agent",即网络层真正可执行的请求。其中,注意到,默认是没有cookie处理的,需要我们在初始化OkhttpClient时配置我们自己的cookieJar

chain.proceed() 执行后,先把响应header中的cookie存入cookieJar(如果有),然后如果没有手动添加请求header "Accept-Encoding: gzip",那么会通过 创建能自动解压的responseBody——GzipSource,接着构建新的response返回。

看起来BridgeInterceptor也是比较好理解的。

CacheInterceptor - 缓存拦截器

CacheInterceptor,缓存拦截器,提供网络请求缓存的存取。 我们发起一个网络请求,如果每次都经过网络的发送和读取,那么效率上是有欠缺的。如果之前有相同的请求已经执行过一次,那么是否可以把它的结果存起来,然后这次请求直接使用呢?CacheInterceptor干的就是这个事,合理使用本地缓存,有效地减少网络开销、减少响应延迟。

在解析CacheInterceptor源码前,先了解下http的缓存机制:

第一次请求:

第一次请求(图片来源于网络) 第二次请求:

第二次请求(图片来源于网络) 上面两张图很好的解释了http的缓存机制:根据 缓存是否过期过期后是否有修改 来决定 请求是否使用缓存。详细说明可点击了解 彻底弄懂HTTP缓存机制及原理(https://www.cnblogs.com/chenqf/p/6386163.html);

CacheInterceptor的intercept方法代码如下:

代码语言:javascript
复制
  public Response intercept(Chain chain) throws IOException {
      //用reques的url 从缓存中 获取响应 作为候选(CacheStrategy决定是否使用)。
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;

    long now = System.currentTimeMillis();
    //根据 request、候选Response 获取缓存策略。
    //缓存策略 将决定是否 使用缓存:strategy.networkRequest为null,不使用网络;strategy.cacheResponse为null,不使用缓存。
    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();
    }

    // 如果不用网络,cacheResponse肯定不为空了,那么就使用缓存了,结束了。
    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }

    //到这里,networkRequest != null (cacheResponse可能null,也可能!null)
    //networkRequest != null,就是要进行网络请求了,所以 拦截器链 就继续 往下处理了~
    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());
      }
    }

    // 如果网络请求返回304,表示服务端资源没有修改,那么就 结合 网络响应和缓存响应,然后更新缓存,返回,结束。
    if (cacheResponse != null) {
      if (networkResponse.code() == HTTP_NOT_MODIFIED) {
        Response response = cacheResponse.newBuilder()
            .headers(combine(cacheResponse.headers(), networkResponse.headers()))//结合header
            .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 {
          //如果是非304,说明服务端资源有更新,就关闭缓存body
        closeQuietly(cacheResponse.body());
      }
    }

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

    if (cache != null) {
      //网络响应可缓存(请求和响应的 头 Cache-Control都不是'no-store')
      if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
        // 写入缓存
        CacheRequest cacheRequest = cache.put(response);
        return cacheWritingResponse(cacheRequest, response);
      }

      //OkHttp默认只会对get请求进行缓存,因为get请求的数据一般是比较持久的,而post一般是交互操作,没太大意义进行缓存
      //不是get请求就移除缓存
      if (HttpMethod.invalidatesCache(networkRequest.method())) {
        try {
          cache.remove(networkRequest);
        } catch (IOException ignored) {
          // The cache cannot be written.
        }
      }
    }

    return response;
  }

整体思路很清晰:使用缓存策略CacheStrategy 来 决定是否使用缓存 及如何使用。

CacheStrategy是如何获取的,这里先不看。需要知道:strategy.networkRequest为null,不使用网络;strategy.cacheResponse为null,不使用缓存

那么按照这个思路,接下来就是一系列的判断:

  1. 若 networkRequest、cacheResponse 都为null,即网络请求、缓存 都不能用,就返回504
  2. 若 networkRequest为null,cacheResponse肯定就不为null,那么就是 不用网络,使用缓存了,就结束了
  3. 若 networkResponse不为null,不管cacheResponse是否null,都会去请求网络,获取网络响应networkResponse
  4. 接着3,若cacheResponse不为null,且networkResponse.code是304,表示服务端资源未修改,缓存是还有效的,那么结合 网络响应和缓存响应,然后更新缓存,结束~
  5. 接着3,若cacheResponse为null 或 cacheResponse不为null但networkResponse.code不是304,那么就写入缓存,返回响应,结束~

继续看,上面判断逻辑都是基于CacheStrategy,这里我们就去看看它是如何生成的,看起来就是一行代码:

代码语言:javascript
复制
    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();

把请求request、候选缓存cacheCandidate传入 工厂类Factory,然后调用get方法,那么就看下吧:

代码语言:javascript
复制
    public Factory(long nowMillis, Request request, Response cacheResponse) {
      this.nowMillis = nowMillis;
      this.request = request;
      this.cacheResponse = cacheResponse;

      if (cacheResponse != null) {
          //获取候选缓存的请求时间、响应时间,从header中获取 过期时间、修改时间、资源标记等(如果有)。
        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);
          }
        }
      }
    }

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

Factory的构造方法内,获取了候选响应的请求时间、响应时间、过期时长、修改时间、资源标记等。

get方法内部先调用了getCandidate()获取到缓存策略实例, 先跟进到 getCandidate方法看看吧:

代码语言:javascript
复制
    private CacheStrategy getCandidate() {
      // 没有缓存:网络请求
      if (cacheResponse == null) {
        return new CacheStrategy(request, null);
      }

      // https,但没有握手:网络请求
      if (request.isHttps() && cacheResponse.handshake() == null) {
        return new CacheStrategy(request, null);
      }

      //网络响应 不可缓存(请求或响应的 头 Cache-Control 是'no-store'):网络请求
      if (!isCacheable(cacheResponse, request)) {
        return new CacheStrategy(request, null);
      }
      //请求头的Cache-Control是no-cache 或者 请求头有"If-Modified-Since"或"If-None-Match":网络请求
      //意思就是 不使用缓存 或者 请求 手动 添加了头部 "If-Modified-Since"或"If-None-Match"
      CacheControl requestCaching = request.cacheControl();
      if (requestCaching.noCache() || hasConditions(request)) {
        return new CacheStrategy(request, null);
      }

      CacheControl responseCaching = cacheResponse.cacheControl();

       //缓存的年龄
      long ageMillis = cacheResponseAge();
      //缓存的有效期
      long freshMillis = computeFreshnessLifetime();
      //比较请求头里有效期,取较小值
      if (requestCaching.maxAgeSeconds() != -1) {
        freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
      }

      //可接受的最小 剩余有效时间(min-fresh标示了客户端不愿意接受 剩余有效期<=min-fresh 的缓存。)
      long minFreshMillis = 0;
      if (requestCaching.minFreshSeconds() != -1) {
        minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
      }
      //可接受的最大过期时间(max-stale指令标示了客户端愿意接收一个已经过期了的缓存,例如 过期了 1小时 还可以用)
      long maxStaleMillis = 0;
      if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
              // 第一个判断:是否要求必须去服务器验证资源状态
              // 第二个判断:获取max-stale值,如果不等于-1,说明缓存过期后还能使用指定的时长
        maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
      }

      //如果响应头没有要求忽略本地缓存 且 整合后的缓存年龄 小于 整合后的过期时间,那么缓存就可以用
      if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
        Response.Builder builder = cacheResponse.newBuilder();
        //没有满足“可接受的最小 剩余有效时间”,加个110警告
        if (ageMillis + minFreshMillis >= freshMillis) {
          builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
        }
        //isFreshnessLifetimeHeuristic表示没有过期时间,那么大于一天,就加个113警告
        long oneDayMillis = 24 * 60 * 60 * 1000L;
        if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
          builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
        }

        return new CacheStrategy(null, builder.build());
      }

      //到这里,说明缓存是过期的
      // 然后 找缓存里的Etag、lastModified、servedDate
      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();

      //conditionalRequest表示 条件网络请求: 有缓存但过期了,去请求网络 询问服务端,还能不能用。能用侧返回304,不能则正常执行网路请求。
      return new CacheStrategy(conditionalRequest, cacheResponse);
    }

同样是进过一些列的判断:

  1. 没有缓存、是https但没有握手、要求不可缓存、忽略缓存或手动配置缓存过期,都是 直接进行 网络请求。
  2. 以上都不满足时,如果缓存没过期,那么就是用缓存(可能要添加警告)。
  3. 如果缓存过期了,但响应头有Etag,Last-Modified,Date,就添加这些header 进行条件网络请求
  4. 如果缓存过期了,且响应头没有设置Etag,Last-Modified,Date,就进行网络请求。

接着回头看get()方法:

代码语言:javascript
复制
    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()获取的缓存策略候选后,又进行了一个判断:使用网络请求 但是 原请求配置了只能使用缓存,按照上面的分析,此时即使有缓存,也是过期的缓存,所以又new了实例,两个值都为null。

好了,到这里okhttp的缓存机制源码就看完了。你会发现,okhttp的缓存机制 是符合 开头 http的缓存机制 那两张图的,只是增加很多细节判断。

另外,注意到,缓存的读写是通过 InternalCache完成的。InternalCache是在创建CacheInterceptor实例时 用client.internalCache()作为参数传入。而InternalCache是okhttp内部使用,类似一个代理,InternalCache的实例是 类Cache的属性。Cache是我们初始化OkHttpClient时传入的。所以如果没有传入Cache实例是没有缓存功能的。

代码语言:javascript
复制
        OkHttpClient client = new OkHttpClient.Builder()
                .cache(new Cache(getExternalCacheDir(),500 * 1024 * 1024))
                .build();

缓存的增删改查,Cache是通过okhttp内部的DiskLruCache实现的,原理和jakewharton的DiskLruCache是一致的,这里就不再叙述,这不是本文重点。

总结

好了,这篇文章到这里内容讲完了,还是挺丰富的。讲解了三个拦截器的工作原理:RetryAndFollowUpInterceptor、BridgeInterceptor、CacheInterceptor,其中CacheInterceptor是请求缓存处理,是比较重要的知识点。目前为止,还没有看到真正的网络连接和请求响应读写,这些内容会在下一篇讲解 — 剩下的两个拦截器 :ConnectInterceptor、CallServerInterceptor。(不然就太长了?)

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-06-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 胡飞洋 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • RetryAndFollowUpInterceptor - 重试、重定向
  • BridgeInterceptor - 桥接拦截器
  • CacheInterceptor - 缓存拦截器
  • 总结
相关产品与服务
云服务器
云服务器(Cloud Virtual Machine,CVM)提供安全可靠的弹性计算服务。 您可以实时扩展或缩减计算资源,适应变化的业务需求,并只需按实际使用的资源计费。使用 CVM 可以极大降低您的软硬件采购成本,简化 IT 运维工作。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档