1. 程式人生 > >OKHttp 3.10原始碼解析(三):快取機制

OKHttp 3.10原始碼解析(三):快取機制

本篇我們來講解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的快取機制就到此結束,下一篇我們講解連線池相關的知識。