從源碼的角度分析 OKHttp3 (三) 緩存策略

前言

因爲以前項目搭建的是 MVP 架構,由RxJava + Glide + OKHttp + Retrofit + Dagger 等開源框架組合而成,以前也都是停留在使用層面上,沒有深刻的研究,最近打算把它們所有攻下,尚未關注的同窗能夠先關注一波,看完這個系列文章,(不論是面試仍是工做中處理問題)相信你都在知道原理的狀況下,處理問題更加駕輕就熟。html

Android 圖片加載框架 Glide 4.9.0 (一) 從源碼的角度分析 Glide 執行流程java

Android 圖片加載框架 Glide 4.9.0 (二) 從源碼的角度分析 Glide 緩存策略android

從源碼的角度分析 Rxjava2 的基本執行流程、線程切換原理web

從源碼的角度分析 OKHttp3 (一) 同步、異步執行流程面試

從源碼的角度分析 OKHttp3 (二) 攔截器的魅力json

從源碼的角度分析 OKHttp3 (三) 緩存策略緩存

Http 緩存基礎

1. 什麼是緩存

緩存是一種保存資源副本並在下次請求時直接使用該副本的技術。說白了,其實就是一種存儲方式。性能優化

2. 爲何使用緩存

經過網絡提取內容既速度緩慢又開銷巨大。 較大的響應須要在客戶端與服務器之間進行屢次往返通訊,這會延遲客戶端得到和處理內容的時間,還會增長訪問者的流量費用。 所以,緩存並重複利用以前獲取的資源的能力成爲性能優化的一個關鍵方面。服務器

3. Http 緩存控制

HTTP/1.1定義的 Cache-Control 頭用來區分對緩存機制的支持狀況, 請求頭和響應頭都支持這個屬性。經過它提供的不一樣的值來定義緩存策略。網絡

語法

指令不區分大小寫,而且具備可選參數,能夠用令牌或者帶引號的字符串語法。多個指令以逗號分隔。

  • 緩存請求指令: 客戶端能夠在HTTP請求中使用的標準 Cache-Control 指令。

    Cache-Control 功能 說明
    max-age= 到期 設置緩存存儲的最大週期,超過這個時間緩存被認爲過時(單位秒)。與Expires相反,時間是相對於請求的時間。
    max-stale[=] 緩存到期時間 代表客戶端願意接收一個已通過期的資源。能夠設置一個可選的秒數,表示響應不能已通過時超過該給定的時間。
    min-fresh= 緩存到期時間 表示客戶端但願獲取一個能在指定的秒數內保持其最新狀態的響應
    no-cache 可緩存性 在發佈緩存副本以前,強制要求緩存把請求提交給原始服務器進行驗證。
    no-store 可緩存性 緩存不該存儲有關客戶端請求或服務器響應的任何內容。
    no-transform 其它 不得對資源進行轉換或轉變。Content-EncodingContent-RangeContent-Type等HTTP頭不能由代理修改。例如,非透明代理或者如Google's Light Mode可能對圖像格式進行轉換,以便節省緩存空間或者減小緩慢鏈路上的流量。no-transform指令不容許這樣作。
    only-if-cached 其它 代表客戶端只接受已緩存的響應,而且不要向原始服務器檢查是否有更新的拷貝
  • 緩存響應指令: 服務器能夠在響應中使用的標準 Cache-Control 指令

    Cache-Control 指令 說明
    must-revalidate 從新驗證和從新加載 一旦資源過時(好比已經超過max-age),在成功向原始服務器驗證以前,緩存不能用該資源響應後續請求。
    no-cache 可緩存性 在發佈緩存副本以前,強制要求緩存把請求提交給原始服務器進行驗證。
    no-store 可緩存性 緩存不該存儲有關客戶端請求或服務器響應的任何內容。
    no-transform 其它 不得對資源進行轉換或轉變。Content-EncodingContent-RangeContent-Type等HTTP頭不能由代理修改。例如,非透明代理或者如Google's Light Mode可能對圖像格式進行轉換,以便節省緩存空間或者減小緩慢鏈路上的流量。no-transform指令不容許這樣作。
    public 可緩存性 代表響應能夠被任何對象(包括:發送請求的客戶端,代理服務器,等等)緩存,即便是一般不可緩存的內容(例如,該響應沒有max-age指令或Expires消息頭)
    private 可緩存性 代表響應只能被單個用戶緩存,不能做爲共享緩存(即代理服務器不能緩存它)。私有緩存能夠緩存響應內容。
    proxy-revalidate 從新驗證和從新加載 與must-revalidate做用相同,但它僅適用於共享緩存(例如代理),並被私有緩存忽略。
    max-age= 緩存到期 設置緩存存儲的最大週期,超過這個時間緩存被認爲過時(單位秒)。與Expires相反,時間是相對於請求的時間。
    s-maxage= 緩存到期 覆蓋max-age或者Expires頭,可是僅適用於共享緩存(好比各個代理),私有緩存會忽略它。
    immutable 從新驗證和從新加載 表示響應正文不會隨時間而改變。資源(若是未過時)在服務器上不發生改變,所以客戶端不該發送從新驗證請求頭(例如If-None-Match或If-Modified-Since)來檢查更新,即便用戶顯式地刷新頁面。在Firefox中,immutable只能被用在 https:// transactions. 有關更多信息,請參閱這裏
  • 示例

    1. 禁止緩存
    //發送以下指令能夠關閉緩存。此外,能夠參考Expires和Pragma消息頭
    Cache-Control: no-cache, no-store, must-revalidate
    複製代碼
    1. 緩存靜態資源
    Cache-Control:public, max-age=120
    複製代碼

OKHttp 緩存策略

前面咱們學習了一些最基本的 Http 緩存基礎,下面咱們來分析 Okhttp 緩存實現,先來分析涉及到的幾個類,最後在以一個實際示例來演示 OKHttp 中的緩存。

1. CacheControl 詳解

OKHttp 裏面的 CacheControl 對應的是 HTTP 裏面的 CacheControl,只不過 OKHTTP 對緩存指令封裝了一層,下面咱們看它的源碼實現:

public final class CacheControl {

  public static final CacheControl FORCE_NETWORK = new Builder().noCache().build();

  public static final CacheControl FORCE_CACHE = new Builder()
      .onlyIfCached()
      .maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS)
      .build();
  
  private final boolean noCache; //對應 HTTP 控制緩存指令的 「no-cache」
  private final boolean noStore; //對應 HTTP 控制緩存指令的 「no-store」
  private final int maxAgeSeconds;//對應 HTTP 控制緩存指令的 「max-age」
  private final int sMaxAgeSeconds;//對應 HTTP 控制緩存指令的 「s-maxage」
  private final boolean isPrivate;//對應 HTTP 控制緩存指令的 「private」
  private final boolean isPublic;//對應 HTTP 控制緩存指令的 「public」
  private final boolean mustRevalidate;//對應 HTTP 控制緩存指令的 「must-revalidate」
  private final int maxStaleSeconds;//對應 HTTP 控制緩存指令的 「max-stale」
  private final int minFreshSeconds;//對應 HTTP 控制緩存指令的 「min-fresh」
  private final boolean onlyIfCached;//對應 HTTP 控制緩存指令的 「only-if-cached」
  private final boolean noTransform;//對應 HTTP 控制緩存指令的 「no-transform」
  private final boolean immutable;//對應 HTTP 控制緩存指令的 「immutable」

  @Nullable String headerValue; 

  private CacheControl(boolean noCache, boolean noStore, int maxAgeSeconds, int sMaxAgeSeconds, boolean isPrivate, boolean isPublic, boolean mustRevalidate, int maxStaleSeconds, int minFreshSeconds, boolean onlyIfCached, boolean noTransform, boolean immutable, @Nullable String headerValue) {
    this.noCache = noCache;
    this.noStore = noStore;
    this.maxAgeSeconds = maxAgeSeconds;
    this.sMaxAgeSeconds = sMaxAgeSeconds;
    this.isPrivate = isPrivate;
    this.isPublic = isPublic;
    this.mustRevalidate = mustRevalidate;
    this.maxStaleSeconds = maxStaleSeconds;
    this.minFreshSeconds = minFreshSeconds;
    this.onlyIfCached = onlyIfCached;
    this.noTransform = noTransform;
    this.immutable = immutable;
    this.headerValue = headerValue;
  }

...//省略構造函數
  
  //主要根據 Request 、 Response Headers 來匹配控制緩存數據的策略
   public static CacheControl parse(Headers headers) {
     
     ...//省略一些 指令匹配
   }
  ....//省略一些 set,get 
}
複製代碼

經過上面的註釋就能夠知道 CacheControl 就是對 Http 中的控制緩存的指令進行封裝.

2. CacheStrategy 詳解

OKHTTP 使用了 CacheStrategy 實現了上面的緩存流程,它根據以前緩存的結果與當前將要發送 Request 的header 進行策略,並得出是否進行請求的結果。

根據 CacheInterceptor 類中的調用咱們先看 CacheStrategy.Factory 函數具體實現

//CacheStrategy.java 
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); //拿到指 令的 key
          String value = headers.value(i);    //拿到指令具體的值
          if ("Date".equalsIgnoreCase(fieldName)) { //根據 header 來匹配
            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);
          }
        }
      }
    }
複製代碼

經過上面代碼咱們知道 CacheStrategy.Factory 函數主要對緩存的響應 header 作一些初始化解析匹配,下面咱們在來看其餘函數。

public CacheStrategy get() {
      //拿到緩存策略
      CacheStrategy candidate = getCandidate();
			//若是當前網絡請求不爲空,而且請求裏面緩存控制配置的是隻用緩存,那麼返回一個請求,緩存都爲空的策略
      if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
        return new CacheStrategy(null, null);
      }
			//返回
      return candidate;
    }		
		//拿到緩存策略
    private CacheStrategy getCandidate() {
      // 返回沒有緩存的策略
      if (cacheResponse == null) {
        return new CacheStrategy(request, null);
      }

      // 若是當前的請求是 HTTPS 而且當前請求的緩存也流失的握手,則返回一個沒有緩存的策略
      if (request.isHttps() && cacheResponse.handshake() == null) {
        return new CacheStrategy(request, null);
      }

      //若是響應不能被緩存,則返回一個沒有緩存的策略
      if (!isCacheable(cacheResponse, request)) {
        return new CacheStrategy(request, null);
      }
			//拿到當前請求頭的控制緩存指令
      CacheControl requestCaching = request.cacheControl();
      //若是請求頭設置了 「no_cache」 則不緩存
      if (requestCaching.noCache() || hasConditions(request)) {
        return new CacheStrategy(request, null);
      }
			//根據當前請求拿到的響應緩存指令對象
      CacheControl responseCaching = cacheResponse.cacheControl();
			//獲取緩存響應的時長
      long ageMillis = cacheResponseAge();
      //獲取上一次響應的刷新時間
      long freshMillis = computeFreshnessLifetime();
			//若是請求中拿到了緩存的最大存活時間
      if (requestCaching.maxAgeSeconds() != -1) {
        //那麼選取 2 則最短的時間賦值給最後刷新的時間
        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());
      }
			//若是響應緩存中沒有配置 「no-cache」,而且 持續時間+最短刷新時間 < 上次刷新時間+最大驗證時間 若是都知足條件的話則能夠緩存
      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 就要知足一些條件
      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();
      //返回知足條件的緩存 Request 策略
      return new CacheStrategy(conditionalRequest, cacheResponse);
    }

		...//省略部分代碼
      
  }
複製代碼

上面 CacheStrategy get() 代碼,能夠說是整個緩存策略中的緩存核心,可是呢?其實這些緩存都是 RFC 標準文檔中定義好的。

經過上面的大量註釋,不用我來總結下流程了吧,相信根據註釋還一遍代碼仍是很容易懂的。

3. CacheInterceptor 攔截器分析

因爲上一篇文章咱們簡單的介紹了 緩存攔截器執行流程,尚未看過了能夠先去看一下從源碼的角度分析 OKHttp3 (二) 攔截器的魅力 ,下面咱們就來說解緩存攔截器中的緩存怎麼 增刪改插

public final class CacheInterceptor implements Interceptor {
  final @Nullable InternalCache cache;

	...//構造函數省略

  @Override public Response intercept(Chain chain) throws IOException {
    //1. get 若是 OKhttpClient 配置了 Cache 那麼就根據當前 Request 的 URL 來進行緩存
    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;
    
		...//省略部分代碼
    //若是網絡請求跟緩存響應爲空的話,就強制返回一個無效緩存,錯誤碼爲 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();
    }

    // 若是 networkRequest 爲空的話,可是響應緩存有效就返回響應緩存
    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }

    //到這裏若是緩存都不知足條件的話,須要從新執行網絡請求
    Response networkResponse = null;
    try {
      //調用下一個攔截器,進行網絡請求
      networkResponse = chain.proceed(networkRequest);
    } finally {
     ...
    }

    // 若是緩存不爲空
    if (cacheResponse != null) {
      //而且響應碼 == 以前定義的 304
      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();

      
        cache.trackConditionalCacheHit();
        //2. cache update 若是知足 cacheResponse != nul 而且網絡請求的響應碼爲 304 就更新緩存
        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)) {
        //3.cache put 存入緩存
        CacheRequest cacheRequest = cache.put(response);
        return cacheWritingResponse(cacheRequest, response);
      }
			
      //檢查緩存是否有效
      if (HttpMethod.invalidatesCache(networkRequest.method())) {
        try {
          //4. cache put 刪除無效緩存
          cache.remove(networkRequest);
        } catch (IOException ignored) {
          
        }
      }
    }
    return response;
  }
複製代碼

那麼根據上面的註釋 1,2,3,4 咱們知道了緩存的增刪查改,這裏總結下流程

一、若是在 OKHttpClient 中配置了 cache,則從緩存中獲取,可是不保證就存在 二、拿到當前請求,緩存拿到緩存策略對象 三、緩存檢測 四、禁止使用網絡(根據緩存策略),緩存又無效,直接返回 五、緩存有效,不使用網絡 六、緩存無效,執行下一個攔截器 七、本地有緩存,根具條件選擇使用哪一個響應 八、使用網絡響應 九、 緩存到本地

上面判斷比較多,總結了一張圖表或許會清晰一點

networkRequest cacheResponse result
Null null only-if-cached (代表不進行網絡請求,且緩存不存在或者過時,必定會返回503錯誤)
Null non-null 不進行網絡請求,直接返回緩存,不請求網絡
non-null(非空) null 須要進行網絡請求,並且緩存不存在或者不可用,直接訪問網絡
non-null non-null Header中包含ETag/Last-Modified標籤,須要在知足條件下請求,仍是須要訪問網絡

OKHTTP 緩存策略到這裏就基本講解完了,下面咱們就以一個示例來實際看下緩存

4. OKHttp 緩存實戰

有網緩存攔截器

/** * 有網時候的緩存 */
    final Interceptor netWorkCacheInterceptor = new Interceptor() {
        @Override
        public Response intercept(Chain chain) throws IOException {
            Request request = chain.request();
            Response response = chain.proceed(request);
            int onlineCacheTime = 30;//在線的時候的緩存過時時間,若是想要不緩存,直接時間設置爲0
            return response.newBuilder()
              			//緩存存活時間的最大限制,最後會根據 CacheControl.parse(headers) 生成一個 //CacheControl
                    .header("Cache-Control", "public, max-age="+onlineCacheTime) 
                    .removeHeader("Pragma")
                    .build();
        }
    };
複製代碼

無網絡緩存攔截器

/** * 沒有網時候的緩存 */
    final Interceptor OfflineCacheInterceptor = new Interceptor() {
        @Override
        public Response intercept(Chain chain) throws IOException {
            Request request = chain.request();
            if (!isNetworkAvailable(getApplicationContext())) {
                int offlineCacheTime = 60;//離線的時候的緩存的過時時間
                request = request.newBuilder()
                        .cacheControl(new CacheControl
                                .Builder()
                                .maxStale(offlineCacheTime, TimeUnit.SECONDS) //最大有效期 60s 
                                .onlyIfCached()
                                .build()
                        ) //兩種方式結果是同樣的,寫法不一樣
// .header("Cache-Control", "public, only-if-cached, max-stale=" + offlineCacheTime)
                        .build();
            }
            return chain.proceed(request);
        }
    };
複製代碼

測試

public void okhttp() {
        String url = "https://wanandroid.com/wxarticle/chapters/json";
        File file = new File(getCacheDir() ,"okhttpCache");
        Cache cache = new Cache(file, 1024 * 1024 * 10); //10M
        OkHttpClient okHttpClient =
                new OkHttpClient.Builder().
                        addInterceptor(OfflineCacheInterceptor).
                        addNetworkInterceptor(netWorkCacheInterceptor).
                        cache(cache).
                        build();

        Request request = new Request.Builder()
                .url(url)
                .get()
                .build();

        okHttpClient.newCall(request).enqueue(new Callback() {

            @Override
            public void onFailure(Call call, IOException e) {
                Log.d(TAG, "responseFail : " + e.getMessage());

            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                Log.d(TAG, "responseBody : " + response.body().string());
            }
        });

    }
複製代碼

上面的代碼就是 OKHttp 的異步 GET 請求,配置了應用、網絡攔截器,下面看下實際效果吧,

KPrHIS.gif

(ps:因爲這裏無網絡不能遠程,就不給動圖了,你們能夠直接拿這 2 個攔截器直接測試)

這裏解釋一下爲何這裏配置兩個攔截器,而不直接一個攔截器在內部判斷就好了,我我的給出的見解是沒有網絡的請況下,應用攔截器在 List 容器的第一個位置,固添加到應用攔截器是爲了不不執行沒必要要的代碼,而有網路的狀況下,我添加到了網絡攔截器,爲了就是能夠在重定向或者添加請求頭以後拿到更加完整的 Request 、Response 對象,從而能夠緩存和更新更多可用的信息。

總結

到這裏 OKHttp 緩存機制分析的差很少了,總結一下,其實有看過這塊源碼的知道 OKHttp 緩存其實不屬於 本身單獨實現,而是用的 JK 大神的 DiskLruCache 開源庫做爲基礎 + OKHttp 設計的緩存策略 = 從而實現了底層緩存技術。

參考

OKHttp源碼解析(六)--中階之緩存基礎

Http 緩存

Cache-Control 指令

相關文章
相關標籤/搜索