美文网首页
OkHttp源码阅读(六) —— 拦截器之CacheInterc

OkHttp源码阅读(六) —— 拦截器之CacheInterc

作者: Sherlock丶Aza | 来源:发表于2021-04-12 08:54 被阅读0次

  有了上一篇HTTP缓存机制的铺垫,现在我们来详细分析下CacheInterceptor的实现原理,实际上在分析CacheInterceptor的工作原理前还应该熟悉一个专门做磁盘缓存的工具类DiskLruCache,它的原理很简单,使用方式和SharedPreferences类似,在这里不赘述了,有兴趣的童鞋可以参看下这篇博客Android DiskLruCache完全解析,硬盘缓存的最佳方案

前言

   之前在逐个分析拦截器的时候,都是直接从intercept()方法开始,在CacheInterceptor分析之前,先要了解两个概念,CahceEntry,那具体都是什么呢? 既然是操作缓存,那么就要有操作缓存的工具类和缓存实体。所以Cache就是操作缓存的工具类,OkHttp是在DiskLruCache基础上进行了封装,实际上的缓存的读取还是使用DiskLruCache,OkHttp添加了一些自己的属性判断而已,至于Entry,顾名思义就是缓存实体类,它是Cache的一个静态内部类,它有很多属性,后边会详细说道。

intercept()方法

  重点来了,一大波代码来袭,不过没什么关系,我做了大量注释,后面还有逐步的分析

 @Override public Response intercept(Chain chain) throws IOException {
    /**首先获取缓存数据,如果有缓存的话,暂且叫cacheCandidate为临时缓存备份,是一个临时的response,
     * 后面要判断临时缓存备份是否可用*/
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;

    long now = System.currentTimeMillis();
    /**缓存策略,根据临时缓存备份和实际请求经过一些的条件判断,最终得到确定一个网络请求networkRequest和一个缓存cacheResponse,
     * 二者都可能为null,
     */
    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    /**networkRequest和cacheResponse在CacheStrategy中有定义**/
    Request networkRequest = strategy.networkRequest;
    Response cacheResponse = strategy.cacheResponse;

    /**trackResponse方法的作用就是记录下网络请求的次数和缓存命中的次数*/
    if (cache != null) {
      cache.trackResponse(strategy);
    }

    /**如果缓存备份不为Null,并且经过缓存策略计算得到的真正的response为Null,
     * 说明该缓存未命中,需要重新请求网络,所以临时缓存备份留着也没什么用了,就
     * 可以关闭资源
     */
    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.
    /**禁网的情况下直接抛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 we don't need the network, we're done.
    /**无网但是有缓存的情况下 直接返回缓存数据*/
    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }


    /**程序走到这一步时,说明networkRequest肯定不会是null,也就是说肯定是有网的状态
     *那么有网的状态如何获取网络response?,还是调用下一组拦截器链来获得。
     */
    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.
    /**如果有缓存并且服务器返回的响应码是304,构建一个新的response,将缓存的内容
     *融合到response里返回,并且更新缓存状态,如果不是304的响应码,不走缓存,缓存response就没
     * 什么用了,关闭资源。
     */
    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();

    /**以下是根据HTTP缓存规则进行判断能否缓存,符合条件的话写入缓存*/
    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;
  }

代码很长,一步一步分析:

读取缓存

/**首先获取缓存数据,如果有缓存的话,暂且叫cacheCandidate为临时缓存备份,是一个临时的response,
     * 后面要判断临时缓存备份是否可用*/
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;

上来第一步读取缓存,这里cache是缓存工具类Cache的对象,之前提到过Cache是对DiskLruCache的封装,我们看下它的putget方法

put方法

@Nullable 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;
    }
    /**http的缓存只是针对于GET方法的,非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;
    }
    /**实例化一个缓存实体,通过DiskLruCache将该实体写入缓存*/
    Entry entry = new Entry(response);
    DiskLruCache.Editor editor = null;
    try {
      /**写入缓存的key是通过url的MD5加密再转换成16进制*/
      editor = cache.edit(key(response.request().url()));
      if (editor == null) {
        return null;
      }
      /**这里只是写入了response的头部内容*/
      entry.writeTo(editor);
      /**真正服务器响应数据通过CacheRequestImpl写入缓存的*/
      return new CacheRequestImpl(editor);
    } catch (IOException e) {
      abortQuietly(editor);
      return null;
    }
  }

代码中注释很详细,我就不赘述了,和SharedPreferences使用类似,只不过多了些条件判断

get方法

 @Nullable Response get(Request request) {
    /**通过url生成key(MD5、HEX)*/
    String key = key(request.url());
    DiskLruCache.Snapshot snapshot;
    Entry entry;
    try {
      /**通过key从内存中读取包装实体类Entry,内存中使用LinkedHashMap,
       * 在通过实体获取到一个Snapshot,这些事内部实现,可以跟进查看
       */
      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;
    }
    /**通过快照snapshot得到一个Response实例*/
    Response response = entry.response(snapshot);

    /**匹配是否是符合要求的,是返回响应,否关闭*/
    if (!entry.matches(request, response)) {
      Util.closeQuietly(response.body());
      return null;
    }

    return response;
  }

get方法的整体流程不再赘述,看注释就可以了,主要介绍下包装实体类Entry,无论是put还是get方法都使用了Entry对象,看下Entry是怎么写入缓存entry.writeTo(editor)和组装response的entry.response(snapshot)

Entry

首先看下Entry的成员变量

private static final String SENT_MILLIS = Platform.get().getPrefix() + "-Sent-Millis";

    /** Synthetic response header: the local time when the response was received. */
    private static final String RECEIVED_MILLIS = Platform.get().getPrefix() + "-Received-Millis";

    private final String url;
    private final Headers varyHeaders;
    private final String requestMethod;
    private final Protocol protocol;
    private final int code;
    private final String message;
    private final Headers responseHeaders;
    private final @Nullable Handshake handshake;
    private final long sentRequestMillis;
    private final long receivedResponseMillis;

很明显都是一个网络请求的基本信息内容,没什么好解释的
接下来看下Entry是怎么写入缓存的

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());
        sink.writeUtf8(handshake.tlsVersion().javaName()).writeByte('\n');
      }
      sink.close();
    }

以上很清晰看到写入缓存的操作都是通过Okio这个库中的输入流写入文件里的,没什么特殊的地方。
接着是读取数据转换成response

public Response response(DiskLruCache.Snapshot snapshot) {
      String contentType = responseHeaders.get("Content-Type");
      String contentLength = responseHeaders.get("Content-Length");
      Request cacheRequest = new Request.Builder()
          .url(url)
          .method(requestMethod, null)
          .headers(varyHeaders)
          .build();
      return new Response.Builder()
          .request(cacheRequest)
          .protocol(protocol)
          .code(code)
          .message(message)
          .headers(responseHeaders)
          .body(new CacheResponseBody(snapshot, contentType, contentLength))
          .handshake(handshake)
          .sentRequestAtMillis(sentRequestMillis)
          .receivedResponseAtMillis(receivedResponseMillis)
          .build();
    }

同样也是很简单的构建,这里不同的是真正响应体body是通过CacheResponseBody进行读取的,我们跟进下CacheResponseBody,看下具体实现

CacheResponseBody(final DiskLruCache.Snapshot snapshot,
        String contentType, String contentLength) {
      this.snapshot = snapshot;
      this.contentType = contentType;
      this.contentLength = contentLength;

      Source source = snapshot.getSource(ENTRY_BODY);
      bodySource = Okio.buffer(new ForwardingSource(source) {
        @Override public void close() throws IOException {
          snapshot.close();
          super.close();
        }
      });
    }

发现还是通过Okio的读写流进行赋值。以上就是Cache缓存工具的内容,原理很简单,完全可以把它当做一个Map或者SharedPreferences想象,接下来我们继续重点分析拦截器的工作流程,继续分析** intercept**方法。

缓存策略配置

/**缓存策略,根据临时缓存备份和实际请求经过一些的条件判断,最终得到确定一个网络请求networkRequest和一个缓存cacheResponse,
     * 二者都可能为null,
     */
    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    /**networkRequest和cacheResponse在CacheStrategy中有定义**/
    Request networkRequest = strategy.networkRequest;
    Response cacheResponse = strategy.cacheResponse;

如果上一步获取到了缓存响应后,则配置缓存策略CacheStrategy,主要是配置CacheStrategynetworkRequestcacheResponse,我们具体看下CacheStrategy的源码:

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缓存的标识,具体HTTP的缓存机制怎么实现的,那就请参考上一篇博客浅析Http中的缓存机制,这个方法的主要就是用来解析这些响应标识的。接下来就是get()方法,获取一个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;
    }

重要逻辑判断在getCandidate(),我们继续跟进

/** Returns a strategy to use assuming the request can use the network. */
    private CacheStrategy getCandidate() {
      // No cached response.
      /**如果没有缓存响应,就返回一个没有响应的策略,这里cacheResponse的赋值在
       * Factory方法传入的临时缓存备份赋值的,实际上就是该请求的缓存响应
       */
      if (cacheResponse == null) {
        return new CacheStrategy(request, null);
      }

      // Drop the cached response if it's missing a required handshake.
      /**如果是HTTPS请求,并且中断了握手,返回一个没有响应的策略*/
      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);
      }
      /**缓存控制,不能缓存的返回一个没有响应的策略,具体判断用到的字段逻辑,参考HTTP的缓存机制*/
      CacheControl requestCaching = request.cacheControl();
      if (requestCaching.noCache() || hasConditions(request)) {
        return new CacheStrategy(request, null);
      }

      CacheControl responseCaching = cacheResponse.cacheControl();
      if (responseCaching.immutable()) {
        return new CacheStrategy(null, cacheResponse);
      }


      /**从这开始下面全都是通过响应头进行判断如何返回策略,具体的逻辑判断条件还是参考HTTP的缓存机制
       *http://www.sherlockaza.com/2017/03/20/2017-03-20-http-cache/
       */
      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;
      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);
    }

注释很详细,不一一解释了,不过可以看到,如果一个缓存响应能不能被使用要经过很多层的筛选。

缓存监测

 /**trackResponse方法的作用就是记录下网络请求的次数和缓存命中的次数*/
    if (cache != null) {
      cache.trackResponse(strategy);
    }

    /**如果缓存备份不为Null,并且经过缓存策略计算得到的真正的response为Null,
     * 说明该缓存未命中,需要重新请求网络,所以临时缓存备份留着也没什么用了,就
     * 可以关闭资源
     */
    if (cacheCandidate != null && cacheResponse == null) {
      closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
    }

记录缓存命中次数和请求次数,不需要的资源释放,closeQuietly后边会多次用到,主要作用是关闭资源,有兴趣的童鞋跟进代码可以看到了,实际就是关闭了response中数据流。

无网无缓存

/**禁网的情况下直接抛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();
    }

禁网并且没有缓存数据,直接返回504

无网有缓存

 // If we don't need the network, we're done.
    /**无网但是有缓存的情况下 直接返回缓存数据*/
    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }

无网有缓存状态直接返回缓存响应。

有网缓存无效

/**程序走到这一步时,说明networkRequest肯定不会是null,也就是说肯定是有网的状态
     *那么有网的状态如何获取网络response?,还是调用下一组拦截器链来获得。
     */
    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.
    /**如果有缓存并且服务器返回的响应码是304,构建一个新的response,将缓存的内容
     *融合到response里返回,并且更新缓存状态,如果不是304的响应码,不走缓存,缓存response就没
     * 什么用了,关闭资源。
     */
    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());
      }
    }

当存在缓存响应时,如果请求响应码是304,说明该缓存有效未做更改,继续使用缓存,则返回该响应,并将缓存命中计数器+1,更新下当前缓存状态,如果响应码不是304,那么说不能使用缓存,就把缓存资源关闭。

使用网络响应

/**走到这一步说明以上都不符合,只能使用网络响应*/
    Response response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();

以上条件都不符合时,使用网络响应

写入缓存

 /**以下是根据HTTP缓存规则进行判断能否缓存,符合条件的话写入缓存*/
    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.
        }
      }
    }

经过一些条件判断,将请求下来的网络响应写入缓存,下次读取使用。

总结

  CacheInterceptor的分析比较之前的分析内容比较多,只要了解HTTP的缓存机制,理解起来也不是很复杂,在Android实际开发中,用GET的请求方式时候非常少,所以用到缓存策略的机会也比较少,所以真正的客户端数据缓存还得靠自己写,但是OkHttp的缓存思想我们还是可以借鉴的。最后还是一个概括的流程图结束:

1.jpeg

相关文章

网友评论

      本文标题:OkHttp源码阅读(六) —— 拦截器之CacheInterc

      本文链接:https://www.haomeiwen.com/subject/aqzykltx.html