Okhttp解析(五)緩存的處理

大家好,之前我們講解了Okhttp網絡數據請求相關的內容,這一節我們講講數據緩存的處理。本節按以下內容講解Okhttp緩存相關的內容。

  1. 緩存的優勢
  2. HTTP的緩存機制
  3. Okhttp的緩存啟用
  4. Okhttp的讀取緩存流程
  5. Okhttp的存儲緩存策略
  6. Okhttp的CacheControl和緩存策略介紹

緩存的優勢

緩存的使用場景很多,通過它可以將數據通過一定的規則存儲起來,再次請求數據的時候就可以快速從緩存中讀取了,緩存有以下優勢。

  1. 減少向服務器請求的次數,減輕服務器的負載。
  2. 加快了本地的響應速度,直接從緩存中取數據比從網絡讀取要快很多。
  3. 提供無網模式下的瀏覽體驗,沒有網絡的情況下也能顯示內容。

HTTP的緩存機制

HTTP本身提供了一套緩存相關的機制。這套機制定義了相關的字段和規則,用來客戶端和服務端進行緩存相關的協商,如響應的數據是否需要緩存,緩存有效期,緩存是否有效,服務器端給出指示,而客戶端則根據服務端的指示做具體的緩存更新和讀取緩存工作。http緩存可以分為兩類:

強制緩存

強制緩存,是直接向緩存數據庫請求數據,如果找到了對應的緩存數據,并且是有效的,就直接返回緩存數據。如果沒有找到或失效了,則向服務器請求數據,返回數據和緩存規則,同時將數據和緩存規則保存到緩存數據庫中。

對比緩存

對比緩存,是先向緩存數據庫獲取緩存數據的標識,然后用該標識去服務器請求該標識對應的數據是否失效,如果沒有失效,服務器會返回304未失效響應,則客戶端使用該標識對應的緩存。如果失效了,服務器會返回最新的數據和緩存規則,客戶端使用返回的最新數據,同時將數據和緩存規則保存到緩存數據庫中。

強制緩存

強制緩存,在緩存數據未失效的情況下,可以直接使用緩存數據,有兩個字段Expires和Cache-Control用于標明失效規則。

Expires

表示過期時間,由服務端返回。那么下次請求數據時,判斷這個Expires過期時間是否已經過了,如果還沒有到過期時間,則使用緩存,如果過了過期時間,則重新請求服務器的數據。Expires格式如下:

Expires: Sat, 11 Nov 2017 10:30:01 GMT

表示到期時間是2017年11月11日10點30分,在這個時間之前可以使用緩存,過了這個時間就要重新請求服務器數據了。

不過因為服務器和客戶端的時間并不是同步的,用一個絕對時間作為過期的標記并不是很明智,所以HTTP1.1之后更多的是Cache-Control,它的控制更加靈活。

Cache-Control

表示緩存的控制,有服務端返回。它有以下幾個取值:

public

表示數據內容都可以被儲存起來,就連有密碼保護的網頁也儲存,安全性很低

private

表示數據內容只能被儲存到私有的cache,僅對某個用戶有效,不能共享

no-cache

表示可以緩存,但是只有在跟WEB服務器驗證了其有效后,才能返回給客戶端,觸發對比緩存

no-store

表示請求和響應都禁止被緩存,強制緩存,對比緩存都不會觸發

max-age

表示返回數據的過期時間

默認情況下是private,也就是不能共享的。Cache-Control格式如下:

Cache-Control:public, max-age=31536000

表示可以被公共緩存,有效時間是1年,也就是說一年時間內,請求該數據時,直接使用緩存,而不用請求服務器了。

對比緩存

對比緩存,表示需要和服務端進行相關信息的對比,由服務器決定是使用緩存還是最新內容,如果服務器判定使用緩存,返回響應嗎304,判定使用最新內容,則返回響應碼200和最新數據。對比緩存的判定字段有兩組:

ETag和If-None-Match

ETag表示資源的一種標識信息,用于標識某個資源,由服務端返回,優先級更高。格式如下:

Etag:"AFY10-6MddXmSerSiXP1ZTiU65VS"

表示該資源的標識是AFY10-6MddXmSerSiXP1ZTiU65VS

然后客戶端再次請求時,加入字段If-None-Match,格式如下:

If-None-Match:"AFY10-6MddXmSerSiXP1ZTiU65VS"

服務端收到請求的該字段時(之前的Etag值),和資源的唯一標識進行對比,如果相同,說明沒有改動,則返回狀態碼304,如果不同,說明資源被改過了,則返回狀態碼200和整個內容數據。

Last-Modified和If-Modified-Since

Last-Modified表示資源的最近修改時間,由服務端返回,優先級更低。格式如下:

Last-Modified: Sat, 11 Nov 2017 10:30:01 GMT

表示上次修改時間是2017年11月11日10點30分。

If-Modified-Since: Sat, 11 Nov 2017 10:30:01 GMT

客戶端請求,表示我指定的這個2017年11月11日10點30分是不是你服務器最新的修改時間。

Last-Modified
由服務器返回,表示響應的數據最近修改的時間。


If-Modified-Since
由客戶端請求,表示詢問服務器這個時間是不是上次修改的時間。如果服務端該資源的修改時間小于等于If-Modified-Since指定的時間,說明資源沒有改動,返回響應狀態碼304,可以使用緩存。如果服務端該資源的修改時間大于If-Modified-Since指定的時間,說明資源又有改動了,則返回響應狀態碼200和最新數據給客戶端,客戶端使用響應返回的最新數據。

Last-Modified字段的值(服務端返回的資源上次修改時間),常常被用于客戶端下次請求時的If-Modified-Since字段中。

兩種緩存的區別

強制緩存的情況下,如果緩存是有效的,則直接使用緩存,而對比緩存不管緩存是否有效,都需要先去和服務器對比是否有新的數據,沒有新的數據才使用緩存數據。

兩種緩存的使用情景

對于強制緩存,服務器通知瀏覽器一個緩存時間,在緩存時間內,下次請求,直接用緩存,不在時間內,執行對比緩存策略。

對于對比緩存,將緩存信息中的Etag和Last-Modified通過請求發送給服務器,由服務器校驗,返回304狀態碼時,瀏覽器直接使用緩存。

HTTP的緩存規則總結

HTTP的緩存規則是優先考慮強制緩存,然后考慮對比緩存。

  1. 首先判斷強制緩存中的數據的是否在有效期內。如果在有效期,則直接使用緩存。如果過了有效期,則進入對比緩存。
  2. 在對比緩存過程中,判斷ETag是否有變動,如果服務端返回沒有變動,說明資源未改變,使用緩存。如果有變動,判斷Last-Modified。
  3. 判斷Last-Modified,如果服務端對比資源的上次修改時間沒有變化,則使用緩存,否則重新請求服務端的數據,并作緩存工作。
image

Okhttp緩存相關類

Okhttp緩存相關的類有如下:

CacheControl(HTTP中的Cache-Control和Pragma緩存控制)

CacheControl是用于描述HTTP的Cache-Control和Pragma字段的類,用于指定緩存的規則。

CacheStrategy(緩存策略類)

CacheStrategy是用于判定使用緩存數據還是網絡請求的決策類。

Cache(緩存類)

對外開放的緩存類,提供了緩存的增刪改查接口。

InternalCache(內部緩存類)

對內使用的緩存類接口,沒有具體實現,只是封裝了Cache的使用。

DiskLruCache(文件化的LRU緩存類)

這是真正實現緩存功能的類,將數據存儲在文件中,并使用LRU規則(由LinkedHashMap實現),控制對緩存文件的增刪改查。

Okhttp緩存的啟用

要開啟使用Okhttp的緩存其實很簡單,只需要給OkHttpClient對象設置一個Cache對象即可,創建一個Cache時指定緩存保存的目錄和緩存最大的大小即可。

//新建一個cache,指定目錄為外部目錄下的okhttp_cache目錄,大小為100M
Cache cache = new Cache(new File(Environment.getExternalStorageDirectory() + "/okhttp_cache/"), 100 * 1024 * 1024);
將cache設置到OkHttpClient中,這樣緩存就開始生效了。
OkHttpClient client = new OkHttpClient.Builder().cache(cache).build();

那么下面我們來看看Okhttp緩存執行的大概流程

Okhttp的緩存流程

Okhttp的緩存流程分為讀取緩存和存儲緩存兩個過程,我們分別分析。

Okhttp讀取緩存流程

讀取使用緩存的流程從HttpEngine的sendRequest發送請求開始。

  1. 首先獲取OkHttpClient的Cache緩存對象,就是之前創建OkHttpClient時設置的Cache。
  2. 然后傳入Request請求到Cache的get方法去查找緩存響應數據Response。
  3. 構造一個緩存策略,傳入Request請求和緩存響應Response,然后調用它的get方法去決策使用網絡請求還是緩存響應。
  4. 策略判定之后,如果是使用緩存,則它的cacheResponse不為空,networkRequest為空,如果使用請求,則相反。然后再將策略給出的這兩個值,繼續處理。
  5. 如果使用請求,但是之前又找到了緩存響應,則要關閉緩存響應資源。
  6. 如果策略得出緩存響應為空,網絡請求也為空,則返回請求不合理的響應。(比如強制使用緩存,但是找不到緩存的情況下)
  7. 如果請求為空,緩存不為空,也就是使用緩存的情況,則使用緩存響應來構造返回的響應數據。
  8. 最后就是只使用網絡請求的情況,走網絡請求路線。
    總的來說就是,先查找是否有可用的Cache,然后通過Cache找到請求對應的緩存,然后將請求和緩存交給緩存策略去判斷使用請求還是緩存,得出結果后,自己再判斷使用緩存還是請求,如果使用緩存,用緩存構造響應直接返回,如果使用請求,那么開始網絡請求流程。
public final class HttpEngine {

  //發送請求
  public void sendRequest() throws RequestException, RouteException, IOException {
    if (cacheStrategy != null) return; // Already sent.
    if (httpStream != null) throw new IllegalStateException();
    //根據用戶請求得到實際的網絡請求
    Request request = networkRequest(userRequest);
    //這里InternalCache就是對Cache的封裝,它的實現在Cache的internalCache中。
    InternalCache responseCache = Internal.instance.internalCache(client);
    //通過Cache的get方法查找緩存響應
    Response cacheCandidate = responseCache != null
        ? responseCache.get(request)
        : null;

    long now = System.currentTimeMillis();
    //構造緩存策略,然后進行策略判斷
    cacheStrategy = new CacheStrategy.Factory(now, request, cacheCandidate).get();
    //策略判定后的網絡請求和緩存響應
    networkRequest = cacheStrategy.networkRequest;
    cacheResponse = cacheStrategy.cacheResponse;
    
    if (responseCache != null) {
      //使用緩存響應的話,記錄一下使用記錄
      responseCache.trackResponse(cacheStrategy);
    }

    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.
    if (networkRequest == null && cacheResponse == null) {
      //強制使用緩存,又找不到緩存,就報不合理請求響應了
      userResponse = new Response.Builder()
          .request(userRequest)
          .priorResponse(stripBody(priorResponse))
          .protocol(Protocol.HTTP_1_1)
          .code(504)
          .message("Unsatisfiable Request (only-if-cached)")
          .body(EMPTY_BODY)
          .build();
      return;
    }
    
    //上面情況處理之后,就是使用緩存返回,還是網絡請求的情況了

    // If we don't need the network, we're done.
    if (networkRequest == null) {
      //使用緩存返回響應
      userResponse = cacheResponse.newBuilder()
          .request(userRequest)
          .priorResponse(stripBody(priorResponse))
          .cacheResponse(stripBody(cacheResponse))
          .build();
      userResponse = unzip(userResponse);
      return;
    }

    //使用網絡請求
    //下面就是網絡請求流程了,略
    ...
  }  
}

接下來我們分析

  1. Cache是如何獲取緩存的。
  2. 緩存策略是如何判斷的。

Cache獲取緩存

從Cache的get方法開始。它按以下步驟進行。

  1. 計算request對應的key值,md5加密請求url得到。
  2. 根據key值去DiskLruCache查找是否存在緩存內容。
  3. 存在緩存的話,創建緩存Entry實體。ENTRY_METADATA代表響應頭信息,ENTRY_BODY代表響應體信息。
  4. 然后根據緩存Entry實體得到響應,其中包含了緩存的響應頭和響應體信息。
  5. 匹配這個緩存響應和請求的信息是否匹配,不匹配的話要關閉資源,匹配的話返回。
public final class Cache implements Closeable, Flushable {
  //獲取緩存
  Response get(Request request) {
    //計算請求對應的key
    String key = urlToKey(request);
    DiskLruCache.Snapshot snapshot;
    Entry entry;
    try {
      //這里從DiskLruCache中讀取緩存信息
      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);
    //判斷緩存響應和請求是否匹配,匹配url,method,和其他響應頭信息
    if (!entry.matches(request, response)) {
      //不匹配的話,關閉響應體
      Util.closeQuietly(response.body());
      return null;
    }

    //返回緩存響應
    return response;
  }
  
  //這里md5加密url得到key值
  private static String urlToKey(Request request) {
    return Util.md5Hex(request.url().toString());
  }
  
}

如果存在緩存的話,在指定的緩存目錄中,會有兩個文件“****.0”和“****.1”,分別存儲某個請求緩存的響應頭和響應體信息。(“****”是url的md5加密值)對應的ENTRY_METADATA響應頭和ENTRY_BODY響應體。緩存的讀取其實是由DiskLruCache來讀取的,DiskLruCache是支持Lru(最近最少訪問)規則的用于磁盤存儲的類,對應LruCache內存存儲。它在存儲的內容超過指定值之后,就會根據最近最少訪問的規則,把最近最少訪問的數據移除,以達到總大小不超過限制的目的。

接下來我們分析CacheStrategy緩存策略是怎么判定的。

CacheStrategy緩存策略

直接看CacheStrategy的get方法。緩存策略是由請求和緩存響應共同決定的。

  1. 如果緩存響應為空,則緩存策略為不使用緩存。
  2. 如果請求是https但是緩存響應沒有握手信息,同上不使用緩存。
  3. 如果請求和緩存響應都是不可緩存的,同上不使用緩存。
  4. 如果請求是noCache,并且又包含If-Modified-Since或If-None-Match,同上不使用緩存。
  5. 然后計算請求有效時間是否符合響應的過期時間,如果響應在有效范圍內,則緩存策略使用緩存。
  6. 否則創建一個新的有條件的請求,返回有條件的緩存策略。
  7. 如果判定的緩存策略的網絡請求不為空,但是只使用緩存,則返回兩者都為空的緩存策略。
public final class CacheStrategy {

    public Factory(long nowMillis, Request request, Response cacheResponse) {
      this.nowMillis = nowMillis;
      //網絡請求和緩存響應
      this.request = request;
      this.cacheResponse = cacheResponse;

      if (cacheResponse != null) {
        //找到緩存響應的響應頭信息
        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 = HeaderParser.parseSeconds(value, -1);
          } else if (OkHeaders.SENT_MILLIS.equalsIgnoreCase(fieldName)) {
            sentRequestMillis = Long.parseLong(value);
          } else if (OkHeaders.RECEIVED_MILLIS.equalsIgnoreCase(fieldName)) {
            receivedResponseMillis = Long.parseLong(value);
          }
        }
      }
    }

    public CacheStrategy get() {
      //獲取判定的緩存策略
      CacheStrategy candidate = getCandidate();

      if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
        // 如果判定的緩存策略的網絡請求不為空,但是只使用緩存,則返回兩者都為空的緩存策略。
        return new CacheStrategy(null, null);
      }

      return candidate;
    }
    
    
    /** Returns a strategy to use assuming the request can use the network. */
    private CacheStrategy getCandidate() {
      // No cached response.
      //如果沒有緩存響應,則返回沒有緩存響應的策略
      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);
      }

      //獲取請求頭中的CacheControl信息
      CacheControl requestCaching = request.cacheControl();
      //如果請求頭中的CacheControl信息是不緩存的,則返回沒有緩存響應的策略
      if (requestCaching.noCache() || hasConditions(request)) {
        return new CacheStrategy(request, null);
      }

      //獲取響應的年齡
      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;
      //獲取緩存響應頭中的CacheControl信息
      CacheControl responseCaching = cacheResponse.cacheControl();
      //如果緩存響應不是必須要再驗證,并且請求有最大過期時間,則用請求的最大過期時間作為最大過期時間
      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());
      }

      //構造一個新的有條件的Request,添加If-None-Match,If-Modified-Since等信息
      Request.Builder conditionalRequestBuilder = request.newBuilder();

      if (etag != null) {
        conditionalRequestBuilder.header("If-None-Match", etag);
      } else if (lastModified != null) {
        conditionalRequestBuilder.header("If-Modified-Since", lastModifiedString);
      } else if (servedDate != null) {
        conditionalRequestBuilder.header("If-Modified-Since", servedDateString);
      }

      Request conditionalRequest = conditionalRequestBuilder.build();
      //根據是否有If-None-Match,If-Modified-Since信息,返回不同的緩存策略
      return hasConditions(conditionalRequest)
          ? new CacheStrategy(conditionalRequest, cacheResponse)
          : new CacheStrategy(conditionalRequest, null);
    }
    
    /**
     * Returns true if the request contains conditions that save the server from sending a response
     * that the client has locally. When a request is enqueued with its own conditions, the built-in
     * response cache won't be used.
     */
    private static boolean hasConditions(Request request) {
      return request.header("If-Modified-Since") != null || request.header("If-None-Match") != null;
    }
}

接來下我們看看CacheControl類里有些什么。

CacheControl

public final class CacheControl {
  
  //表示這是一個優先使用網絡驗證,驗證通過之后才可以使用緩存的緩存控制,設置了noCache
  public static final CacheControl FORCE_NETWORK = new Builder().noCache().build();

  //表示這是一個優先先使用緩存的緩存控制,設置了onlyIfCached和maxStale的最大值
  public static final CacheControl FORCE_CACHE = new Builder()
      .onlyIfCached()
      .maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS)
      .build();

  //以下的字段都是HTTP中Cache-Control字段相關的值
  private final boolean noCache;
  private final boolean noStore;
  private final int maxAgeSeconds;
  private final int sMaxAgeSeconds;
  private final boolean isPrivate;
  private final boolean isPublic;
  private final boolean mustRevalidate;
  private final int maxStaleSeconds;
  private final int minFreshSeconds;
  private final boolean onlyIfCached;
  private final boolean noTransform;
  
  //解析頭文件中的相關字段,得到該緩存控制類
  public static CacheControl parse(Headers headers) {
    ...
  }
  
}

可以發現,它就是用于描述響應的緩存控制信息。

然后我們再看看Okhttp存儲緩存是怎么進行的。

Okhttp存儲緩存流程

存儲緩存的流程從HttpEngine的readResponse發送請求開始的。

public final class HttpEngine {
  /**
   * Flushes the remaining request header and body, parses the HTTP response headers and starts
   * reading the HTTP response body if it exists.
   */
  public void readResponse() throws IOException {
    //讀取響應,略
    ...

    // 判斷響應信息中包含響應體
    if (hasBody(userResponse)) {
      // 如果緩存的話,緩存響應頭信息
      maybeCache();
      //緩存響應體信息,同時zip解壓縮響應數據
      userResponse = unzip(cacheWritingResponse(storeRequest, userResponse));
    }
  }
  
  // 如果緩存的話,緩存響應頭信息
  private void maybeCache() throws IOException {
    InternalCache responseCache = Internal.instance.internalCache(client);
    if (responseCache == null) return;

    // Should we cache this response for this request?
    if (!CacheStrategy.isCacheable(userResponse, networkRequest)) {
      if (HttpMethod.invalidatesCache(networkRequest.method())) {
        try {
          responseCache.remove(networkRequest);
        } catch (IOException ignored) {
          // The cache cannot be written.
        }
      }
      return;
    }

    // Offer this request to the cache.
    //這里將響應頭信息緩存到緩存文件中,對應緩存文件“\*\*\*\*.0”
    storeRequest = responseCache.put(stripBody(userResponse));
  }
  
  /**
   * Returns a new source that writes bytes to {@code cacheRequest} as they are read by the source
   * consumer. This is careful to discard bytes left over when the stream is closed; otherwise we
   * may never exhaust the source stream and therefore not complete the cached response.
   */
  //緩存響應體信息
  private Response cacheWritingResponse(final CacheRequest cacheRequest, Response response)
      throws IOException {
    // Some apps return a null body; for compatibility we treat that like a null cache request.
    if (cacheRequest == null) return response;
    Sink cacheBodyUnbuffered = cacheRequest.body();
    if (cacheBodyUnbuffered == null) return response;

    final BufferedSource source = response.body().source();
    final BufferedSink cacheBody = Okio.buffer(cacheBodyUnbuffered);

    Source cacheWritingSource = new Source() {
      boolean cacheRequestClosed;

      //這里就是從響應體體讀取數據,保存到緩存文件中,對應緩存文件“\*\*\*\*.1”
      @Override public long read(Buffer sink, long byteCount) throws IOException {
        long bytesRead;
        try {
          bytesRead = source.read(sink, byteCount);
        } catch (IOException e) {
          if (!cacheRequestClosed) {
            cacheRequestClosed = true;
            cacheRequest.abort(); // Failed to write a complete cache response.
          }
          throw e;
        }

        if (bytesRead == -1) {
          if (!cacheRequestClosed) {
            cacheRequestClosed = true;
            cacheBody.close(); // The cache response is complete!
          }
          return -1;
        }

        sink.copyTo(cacheBody.buffer(), sink.size() - bytesRead, bytesRead);
        cacheBody.emitCompleteSegments();
        return bytesRead;
      }

      @Override public Timeout timeout() {
        return source.timeout();
      }

      @Override public void close() throws IOException {
        if (!cacheRequestClosed
            && !discard(this, HttpStream.DISCARD_STREAM_TIMEOUT_MILLIS, MILLISECONDS)) {
          cacheRequestClosed = true;
          cacheRequest.abort();
        }
        source.close();
      }
    };

    return response.newBuilder()
        .body(new RealResponseBody(response.headers(), Okio.buffer(cacheWritingSource)))
        .build();
  }
}

可以看到這里先通過maybeCache寫入了響應頭信息,再通過cacheWritingResponse寫入了響應體信息。我們再進去看Cache的put方法實現。

private CacheRequest put(Response response) throws IOException {
    String requestMethod = response.request().method();

    // 響應的請求方法不支持緩存,只有GET方法支持緩存
    if (HttpMethod.invalidatesCache(response.request().method())) {
      try {
        remove(response.request());
      } catch (IOException ignored) {
        // The cache cannot be written.
      }
      return null;
    }
    // 同樣,請求只支持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 (OkHeaders.hasVaryAll(response)) {
      return null;
    }

    //開始緩存
    Entry entry = new Entry(response);
    DiskLruCache.Editor editor = null;
    try {
      editor = cache.edit(urlToKey(response.request()));
      if (editor == null) {
        return null;
      }
      entry.writeTo(editor);
      return new CacheRequestImpl(editor);
    } catch (IOException e) {
      abortQuietly(editor);
      return null;
    }
}

我們繼續看Cache的writeTo方法,可以看到是寫入一些響應頭信息。

public void writeTo(DiskLruCache.Editor editor) throws IOException {
      BufferedSink sink = Okio.buffer(editor.newSink(ENTRY_METADATA));

      sink.writeUtf8(url);
      sink.writeByte('\n');
      sink.writeUtf8(requestMethod);
      sink.writeByte('\n');
      sink.writeDecimalLong(varyHeaders.size());
      sink.writeByte('\n');
      for (int i = 0, size = varyHeaders.size(); i < size; i++) {
        sink.writeUtf8(varyHeaders.name(i));
        sink.writeUtf8(": ");
        sink.writeUtf8(varyHeaders.value(i));
        sink.writeByte('\n');
      }

      sink.writeUtf8(new StatusLine(protocol, code, message).toString());
      sink.writeByte('\n');
      sink.writeDecimalLong(responseHeaders.size());
      sink.writeByte('\n');
      for (int i = 0, size = responseHeaders.size(); i < size; i++) {
        sink.writeUtf8(responseHeaders.name(i));
        sink.writeUtf8(": ");
        sink.writeUtf8(responseHeaders.value(i));
        sink.writeByte('\n');
      }

      if (isHttps()) {
        sink.writeByte('\n');
        sink.writeUtf8(handshake.cipherSuite().javaName());
        sink.writeByte('\n');
        writeCertList(sink, handshake.peerCertificates());
        writeCertList(sink, handshake.localCertificates());
        // The handshake’s TLS version is null on HttpsURLConnection and on older cached responses.
        if (handshake.tlsVersion() != null) {
          sink.writeUtf8(handshake.tlsVersion().javaName());
          sink.writeByte('\n');
        }
      }
      sink.close();
    }

到這里Okhttp緩存的讀取和存儲流程我們就清楚了。可以說,緩存的使用策略基本都是按照HTTP的緩存定義來實現的,所以對HTTP緩存相關字段的理解是很重要的。然后關于DiskLruCache是如何管理緩存文件的,這個其實也很好理解,首先的原則就是按照LRU這種最近最少使用刪除的原則,當總的大小超過限定大小后,刪除最近最少使用的緩存文件,它的LRU算法是使用LinkedHashMap進行維護的,這樣來保證,保留的緩存文件都是更常使用的。具體實現大家可以分析DiskLruCache和LinkedHashMap的實現原理。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,310評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,951評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,566評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,055評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,303評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,799評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,683評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,899評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,135評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,528評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,844評論 2 372

推薦閱讀更多精彩內容