OkHttpClient原始碼分析(四)—— CacheInterceptor

chaychan發表於2019-01-04

CacheInterceptor

介紹完快取之後,現在開始介紹快取攔截器CacheInterceptor了,同樣也是檢視其intercept()方法,這裡邊上片段程式碼邊解析,化整為零:

@Override public Response intercept(Chain chain) throws IOException {
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;
    ...
}
複製程式碼

首先通過判斷快取物件是否為null,如果不為null則根據傳入的Chain物件的request獲取快取的Response。

@Override public Response intercept(Chain chain) throws IOException {
      ...
      CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
      Request networkRequest = strategy.networkRequest;
      Response cacheResponse = strategy.cacheResponse;
      ...
}
複製程式碼

建立快取策略物件CacheStrategy,CacheStrategy內部維護了一個Request和一個Response,該類主要是用於解決到底使用網路還是快取,亦或是兩者皆用:

 /** 如果不使用網路,則 networkRequest為 null */
  public final @Nullable Request networkRequest;
  /** 如果不使用快取,則 cacheResponse為 null */
  public final @Nullable Response cacheResponse;
複製程式碼

關於快取策略的建立,我們檢視CacheStrategy的內部類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;
 }
複製程式碼

該方法比較簡單,建立CacheStrategy的主要的邏輯是在getCandidate()方法中:

 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();
      if (requestCaching.noCache() || hasConditions(request)) {
        return new CacheStrategy(request, null);
      }
      
      //上述需要網路請求返回的 CacheStrategy 第一個引數傳入request
        
      //如果快取是不受影響的,CacheStrategy傳入cacheResponse
      CacheControl responseCaching = cacheResponse.cacheControl();
      if (responseCaching.immutable()) {
        return new CacheStrategy(null, cacheResponse);
      }

      
      //可以快取,新增請求頭資訊
      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());
      }

      ...
    }

複製程式碼

介紹完CacheStrategy物件和建立CacheStrategy的過程後,接著分析intercept()方法:

@Override public Response intercept(Chain chain) throws IOException {
    ...
    
    //如果有快取,更新統計指標, 增加命中率
    if (cache != null) {
      cache.trackResponse(strategy);
    }

    ...
   
    //如果當前沒有網路且找不到快取
    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();
    }
    
    ...
}

複製程式碼

如果有快取,通過呼叫CacheStrategy的trackResponse()方法更新統計指標, 更新網路請求數和命中快取數;

接著通過判斷CacheStrategy的networkRequest和cacheResponse,如果二者同時為null,即當前沒有網路且沒有快取,則構造一個Response物件並返回,其中狀態碼為504,並設定了提示的message資訊。

接著分析:

@Override public Response intercept(Chain chain) throws IOException {
    ...
      //如果不使用網路請求,直接返回快取的Response
     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());
      }
    }
    
    
    //本地有快取
    if (cacheResponse != null) {
      //伺服器返回狀態碼為HTTP_NOT_MODIFIED(304)
      if (networkResponse.code() == HTTP_NOT_MODIFIED) {
        //使用快取資料
        Response response = cacheResponse.newBuilder()
             ...
            .build();
            ...
      } else {
        closeQuietly(cacheResponse.body());
      }
    }
    
    
    //如果伺服器資源已經修改,使用網路響應返回的最新資料
    Response response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();
        
    
    //如果有快取
    if (cache != null) {
      //htpp 頭部有響應體且需要快取
      if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
        CacheRequest cacheRequest = cache.put(response);
        return cacheWritingResponse(cacheRequest, response);
      }
      //請求方法不符合能夠快取
      if (HttpMethod.invalidatesCache(networkRequest.method())) {
        try {
          //移除對應的request
          cache.remove(networkRequest);
        } catch (IOException ignored) {
        }
      }
    }

    return response;
  }
複製程式碼

當伺服器返回狀態碼為HTTP_NOT_MODIFIED即304時,說明快取還沒過期或伺服器資源沒修改,此時返回快取;如果伺服器資源修改了,則使用網路響應返回的最新資料構造Response,接著將最新的資料快取並移除無效的快取。

總結CacheInterceptor主要做的操作:

  1. 從快取中獲取Reponse物件,賦值給caceResponse,如果找不到快取則為null;
  2. 根據當前請求request和caceResponse 構建一個CacheStrategy物件;
  3. CacheStrategy這個策略物件將根據相關規則來決定cacheResponse和Request是否有效,如果無效則分別將cacheResponse和request設定為null;
  4. 如果request和cacheResponse都為null,即沒有網路且沒有快取,直接返回一個狀態碼為504的空Respone物件;
  5. 如果resquest為null而cacheResponse不為null,即沒有網路且有快取,則直接返回cacheResponse物件;
  6. 執行下一個攔截器的intercept()方法進行網路請求,獲取response;
  7. 如果伺服器資源沒有過期(狀態碼304)且存在快取,則返回快取;
  8. 如果伺服器資源有修改,則將返回的最新資料進行快取並移除掉無效的快取,最後返回response物件給上一個攔截器。

下一篇將講解五大攔截器中的最後兩個攔截器, ConnectInterceptor和CallServerInterceptor,感興趣的朋友可以繼續閱讀:

OkHttpClient原始碼分析(五)—— ConnectInterceptor和CallServerInterceptor

相關文章