OkHttp源碼閱讀(六) —— 攔截器之CacheInterceptor

??有了上一篇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
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容