OkHttp3.0解析——談談內部的快取策略

晨雨細曲發表於2018-11-15

前言

合理的利用本地的快取策略,可以有效的減少網路請求時候的網路開銷,減少響應的延遲。而在OkHttp3.0中的快取主要作用在快取攔截器CacheInterceptor裡面。所以現在我們就具體分析下CacheInterceptor中對快取的具體操作。

CacheInterceptor

我們都知道,OkHttp的核心或者說精華部分就是其強大的攔截器功能,幾乎你在使用他的時候都是一些攔截器在背後默默幫你做一些操作。而快取攔截器也正是在背後默默幫你對資料的快取作著操作。在瞭解快取攔截器之前,我們必須先理解內部的三個東西。

Cache: 快取管理器。其內部擁有一個DiskLruCache演算法在操作,將獲取到的快取寫入到系統檔案當中去。

CacheStrategy: 快取策略。內部維護了request與response。通過策略來判斷到底是從網路端獲取資料還是從本地快取中獲取資料亦或者兩者並用。

CacheStrategyFactory: 快取工廠。通過此方法來獲取到快取策略這個物件。

實際的快取是在CacheInterceptor這個類中的intercept方法中完成的,那麼我們下面來看看這個方法中具體的操作邏輯。

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

    //如果當前的快取不符合要求,則將其close
    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());
      }
    }

    // 如果既有快取,同時又發起了請求,說明此時是一個Conditional Get請求
    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();

    if (cache != null) {
      if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {

        // 將網路請求之後的結果寫入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;
  }
複製程式碼

分析上面程式碼可以看到,首先我們會從快取策略工廠(CacheStrategyFactory)中獲取快取策略(CacheStrategyFactory)。之後做幾次判斷,如果本地有快取則直接獲取快取,如果快取和網路都不能使用,則丟擲504連線超時的異常。如果本地沒有快取但是網路可以使用,則呼叫networkResponse來請求網路資料,並且將網路資料通過cacheWritingResponse()寫入diskLruCache中。到此整個快取就算是全部弄完了。

DiskLruCache:

Cache內部通過DiskLruCache管理cache在檔案系統層面的建立,讀取,清理等等工作,接下來看下DiskLruCache的主要邏輯:

public final class DiskLruCache implements Closeable, Flushable {
  
  final FileSystem fileSystem;
  final File directory;
  private final File journalFile;
  private final File journalFileTmp;
  private final File journalFileBackup;
  private final int appVersion;
  private long maxSize;
  final int valueCount;
  private long size = 0;
  BufferedSink journalWriter;
  final LinkedHashMap<String, Entry> lruEntries = new LinkedHashMap<>(0, 0.75f, true);

  // Must be read and written when synchronized on 'this'.
  boolean initialized;
  boolean closed;
  boolean mostRecentTrimFailed;
  boolean mostRecentRebuildFailed;

  /**
   * To differentiate between old and current snapshots, each entry is given a sequence number each
   * time an edit is committed. A snapshot is stale if its sequence number is not equal to its
   * entry's sequence number.
   */
  private long nextSequenceNumber = 0;

  /** Used to run 'cleanupRunnable' for journal rebuilds. */
  private final Executor executor;
  private final Runnable cleanupRunnable = new Runnable() {
    public void run() {
        ......
    }
  };
  ...
  }
複製程式碼

DiskLruCache內部日誌檔案,對cache的每一次讀寫都對應一條日誌記錄,DiskLruCache通過分析日誌分析和建立cache

日誌檔案的應用場景主要有四個:

  • DiskCacheLru初始化時通過讀取日誌檔案建立cache容器:lruEntries。同時通過日誌過濾操作不成功的cache項。相關邏輯在DiskLruCache.readJournalLine,DiskLruCache.processJournal
  • 初始化完成後,為避免日誌檔案不斷膨脹,對日誌進行重建精簡,具體邏輯在DiskLruCache.rebuildJournal
  • 每當有cache操作時將其記錄入日誌檔案中以備下次初始化時使用
  • 當冗餘日誌過多時,通過呼叫cleanUpRunnable執行緒重建日誌

每一個DiskLruCache.Entry對應一個cache記錄

一個Entry主要由以下幾部分構成:

  • key:每個cache都有一個key作為其識別符號。當前cache的key為其對應URL的MD5字串
  • cleanFiles/dirtyFiles:每一個Entry對應多個檔案,其對應的檔案數由DiskLruCache.valueCount指定。當前在OkHttp中valueCount為2。即每個cache對應2個cleanFiles,2個dirtyFiles。其中第一個cleanFiles/dirtyFiles記錄cache的meta資料(如URL,建立時間,SSL握手記錄等等),第二個檔案記錄cache的真正內容。cleanFiles記錄處於穩定狀態的cache結果,dirtyFiles記錄處於建立或更新狀態的cache
  • currentEditor:entry編輯器,對entry的所有操作都是通過其編輯器完成。編輯器內部新增了同步鎖

總結

總結起來DiskLruCache主要有以下幾個特點:

  • 通過LinkedHashMap實現LRU替換
  • 通過本地維護Cache操作日誌保證Cache原子性與可用性,同時為防止日誌過分膨脹定時執行日誌精簡
  • 每一個Cache項對應兩個狀態副本:DIRTY,CLEAN。CLEAN表示當前可用狀態Cache,外部訪問到的cache快照均為CLEAN狀態;DIRTY為更新態Cache。由於更新和建立都只操作DIRTY狀態副本,實現了Cache的讀寫分離
  • 每一個Cache項有四個檔案,兩個狀態(DIRTY,CLEAN),每個狀態對應兩個檔案:一個檔案儲存Cache meta資料,一個檔案儲存Cache內容資料

有興趣可以關注我的小專欄,學習更多職場產品思考知識:小專欄

OkHttp3.0解析——談談內部的快取策略

相關文章