你想要的系列:網絡請求框架OkHttp3全解系列 - (三)攔截器詳解1:重試重定向、橋、緩存(重點)

在本系列的上一篇文章中,咱們走讀了一遍okhttp的源碼,初步瞭解了這個強大的網絡框架的基本執行流程。html

不過,上一篇文章只能說是比較粗略地閱讀了okhttp整個執行流程方面的源碼,搞明白了okhttp的基本工做原理,但並無去深刻分析細節(事實上也不可能在一篇文章中深刻分析每一處源碼的細節)。那麼本篇文章,咱們對okhttp進行深刻地分析,慢慢將okhttp中的各項功能進行全面掌握。java

今天文章中的源碼都建在上一篇源碼分析的基礎之上,尚未看過上一篇文章的朋友,建議先去閱讀 網絡請求框架OkHttp3全解系列 - (二)OkHttp的工做流程分析 。這篇中咱們知道,網絡請求的真正執行是經過攔截器鏈關聯的各個攔截器進行處理,每一個攔截器負責不一樣的功能,下面將詳細分析每一個攔截器,包括重要知識點——緩存、鏈接池。緩存

先回顧下RealCall的getResponseWithInterceptorChain方法——攔截器的添加以及攔截器鏈的執行:服務器

Response getResponseWithInterceptorChain() throws IOException {
    // 建立一系列攔截器
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());
    interceptors.add(new RetryAndFollowUpInterceptor(client));
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    interceptors.add(new CacheInterceptor(client.internalCache()));
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));

    Interceptor.Chain chain = new RealInterceptorChain(interceptors, transmitter, null, 0,
        originalRequest, this, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());
    ...
      //攔截器鏈的執行
      Response response = chain.proceed(originalRequest);
      ...
      return response;
    ...
  }
複製代碼

RetryAndFollowUpInterceptor - 重試、重定向

若是請求建立時沒有添加應用攔截器,那麼第一個攔截器就是RetryAndFollowUpInterceptor,意爲「重試和跟進攔截器」,做用是鏈接失敗後進行重試、對請求結果跟進後進行重定向。直接看它的intercept方法:微信

@Override public Response intercept(Chain chain) throws IOException {
    Request request = chain.request();
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Transmitter transmitter = realChain.transmitter();

    int followUpCount = 0;
    Response priorResponse = null;
    
    while (true) {
      //準備鏈接
      transmitter.prepareToConnect(request);
      
      if (transmitter.isCanceled()) {
        throw new IOException("Canceled");
      }

      Response response;
      boolean success = false;
      try {
        //繼續執行下一個Interceptor
        response = realChain.proceed(request, transmitter, null);
        success = true;
      } catch (RouteException e) {
        // 鏈接路由異常,此時請求還未發送。嘗試恢復~
        if (!recover(e.getLastConnectException(), transmitter, false, request)) {
          throw e.getFirstConnectException();
        }
        continue;
      } catch (IOException e) {
        // IO異常,請求可能已經發出。嘗試恢復~
        boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
        if (!recover(e, transmitter, requestSendStarted, request)) throw e;
        continue;
      } finally {
        // 請求沒成功,釋放資源。
        if (!success) {
          transmitter.exchangeDoneDueToException();
        }
      }

      // 關聯上一個response
      if (priorResponse != null) {
        response = response.newBuilder()
            .priorResponse(priorResponse.newBuilder()
                    .body(null)
                    .build())
            .build();
      }

      Exchange exchange = Internal.instance.exchange(response);
      Route route = exchange != null ? exchange.connection().route() : null;
      //跟進結果,主要做用是根據響應碼處理請求,返回Request不爲空時則進行重定向處理-拿到重定向的request
      Request followUp = followUpRequest(response, route);

	  //followUp爲空,不須要重試,直接返回
      if (followUp == null) {
        if (exchange != null && exchange.isDuplex()) {
          transmitter.timeoutEarlyExit();
        }
        return response;
      }

	  //followUpBody.isOneShot,不須要重試,直接返回
      RequestBody followUpBody = followUp.body();
      if (followUpBody != null && followUpBody.isOneShot()) {
        return response;
      }

      closeQuietly(response.body());
      if (transmitter.hasExchange()) {
        exchange.detachWithViolence();
      }

	  //最多重試20次
      if (++followUpCount > MAX_FOLLOW_UPS) {
        throw new ProtocolException("Too many follow-up requests: " + followUpCount);
      }
      //賦值,以便再次請求
      request = followUp;
      priorResponse = response;
    }
  }
複製代碼

使用while進行循環cookie

先用transmitter.prepareToConnect(request)進行鏈接準備。transmitter在上一篇有提到,意爲發射器,是應用層和網絡層的橋樑,在進行 鏈接、真正發出請求和讀取響應中起到很重要的做用。看下prepareToConnect方法:網絡

public void prepareToConnect(Request request) {
    if (this.request != null) {
      if (sameConnection(this.request.url(), request.url()) && exchangeFinder.hasRouteToTry()) {
        return; //已有相同鏈接
      }
      ...
    }

    this.request = request;
    //建立ExchangeFinder,目的是爲獲取鏈接作準備
    this.exchangeFinder = new ExchangeFinder(this, connectionPool, createAddress(request.url()),
        call, eventListener);
  }
複製代碼

主要是建立ExchangeFinder梳理賦值給transmitter.exchangeFinder。ExchangeFinder是交換查找器,做用是獲取請求的鏈接。這裏先了解下,後面會具體說明。app

接着調用realChain.proceed 繼續傳遞請求給下一個攔截器、從下一個攔截器獲取原始結果。若是此過程發生了 鏈接路由異常 或 IO異常,就會調用recover判斷是否進行重試恢復。看下recover方法:框架

private boolean recover(IOException e, Transmitter transmitter, boolean requestSendStarted, Request userRequest) {
    // 應用層禁止重試,就不重試
    if (!client.retryOnConnectionFailure()) return false;

    // 不能再次發送請求,就不重試
    if (requestSendStarted && requestIsOneShot(e, userRequest)) return false;

    // 發生的異常是致命的,就不重試
    if (!isRecoverable(e, requestSendStarted)) return false;

    // 沒有路由能夠嘗試,就不重試
    if (!transmitter.canRetry()) return false;

    return true;
  }
複製代碼

若是recover方法返回true,那麼就會進入下一次循環,從新請求。ide

若是realChain.proceed沒有發生異常,返回告終果response,就會使用followUpRequest方法跟進結果並構建重定向request。 若是不用跟進處理(例如響應碼是200),則返回null。看下followUpRequest方法:

private Request followUpRequest(Response userResponse, @Nullable Route route) throws IOException {
  ...
    int responseCode = userResponse.code();
  ...
    switch (responseCode) {
      ...
      case HTTP_PERM_REDIRECT:
      case HTTP_TEMP_REDIRECT:
        if (!method.equals("GET") && !method.equals("HEAD")) {
          return null;
        }
      case HTTP_MULT_CHOICE:
      case HTTP_MOVED_PERM:
      case HTTP_MOVED_TEMP:
      case HTTP_SEE_OTHER:
        if (!client.followRedirects()) return null;

        String location = userResponse.header("Location");
        if (location == null) return null;
        HttpUrl url = userResponse.request().url().resolve(location);

        if (url == null) return null;
        boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme());
        if (!sameScheme && !client.followSslRedirects()) return null;

        Request.Builder requestBuilder = userResponse.request().newBuilder();
        if (HttpMethod.permitsRequestBody(method)) {
          final boolean maintainBody = HttpMethod.redirectsWithBody(method);
          if (HttpMethod.redirectsToGet(method)) {
            requestBuilder.method("GET", null);
          } else {
            RequestBody requestBody = maintainBody ? userResponse.request().body() : null;
            requestBuilder.method(method, requestBody);
          }
          if (!maintainBody) {
            requestBuilder.removeHeader("Transfer-Encoding");
            requestBuilder.removeHeader("Content-Length");
            requestBuilder.removeHeader("Content-Type");
          }
        }
        if (!sameConnection(userResponse.request().url(), url)) {
          requestBuilder.removeHeader("Authorization");
        }

        return requestBuilder.url(url).build();

      case HTTP_CLIENT_TIMEOUT:
        ...
      default:
        return null;
    }
  }
複製代碼

主要就是根據響應碼判斷若是須要重定向,就從響應中取出重定向的url並構建新的Request並返回出去。

往下看,還有個判斷:最多重試20次。到這裏,RetryAndFollowUpInterceptor就講完了,仍是比較簡單的,主要就是鏈接失敗的重試、跟進處理重定向。

BridgeInterceptor - 橋接攔截器

接着是 BridgeInterceptor ,意爲 橋攔截器,至關於 在 請求發起端 和 網絡執行端 架起一座橋,把應用層發出的請求 變爲 網絡層認識的請求,把網絡層執行後的響應 變爲 應用層便於應用層使用的結果。 看代碼:

//橋攔截器
public final class BridgeInterceptor implements Interceptor {

  //Cookie管理器,初始化OkhttpClient時建立的,默認是CookieJar.NO_COOKIES
  private final CookieJar cookieJar;

  public BridgeInterceptor(CookieJar cookieJar) {
    this.cookieJar = cookieJar;
  }

  @Override public Response intercept(Chain chain) throws IOException {
    Request userRequest = chain.request();
    Request.Builder requestBuilder = userRequest.newBuilder();
    
    RequestBody body = userRequest.body();
    if (body != null) {
      MediaType contentType = body.contentType();
      if (contentType != null) {
        requestBuilder.header("Content-Type", contentType.toString());
      }

      long contentLength = body.contentLength();
      //若是已經知道body的長度,就添加頭部"Content-Length"
      if (contentLength != -1) {
        requestBuilder.header("Content-Length", Long.toString(contentLength));
        requestBuilder.removeHeader("Transfer-Encoding");
      } else {
      //若是不知道body的長度,就添加頭部"Transfer-Encoding",表明這個報文采用了分塊編碼。這時,報文中的實體須要改成用一系列分塊來傳輸。具體理解請參考:[HTTP Transfer-Encoding介紹](https://blog.csdn.net/Dancen/article/details/89957486)
        requestBuilder.header("Transfer-Encoding", "chunked");
        requestBuilder.removeHeader("Content-Length");
      }
    }

    if (userRequest.header("Host") == null) {
      requestBuilder.header("Host", hostHeader(userRequest.url(), false));
    }

    if (userRequest.header("Connection") == null) {
      requestBuilder.header("Connection", "Keep-Alive");
    }

	//"Accept-Encoding: gzip",表示接受:返回gzip編碼壓縮的數據
    // 若是咱們手動添加了 "Accept-Encoding: gzip" ,那麼下面的if不會進入,transparentGzip是false,就須要咱們本身處理數據解壓。
    //若是 沒有 手動添加"Accept-Encoding: gzip" ,transparentGzip是true,同時會自動添加,並且後面也會自動處理解壓。
    boolean transparentGzip = false;
    if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
      transparentGzip = true;
      requestBuilder.header("Accept-Encoding", "gzip");
    }
	//從cookieJar中獲取cookie,添加到header
    List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
    if (!cookies.isEmpty()) {
      requestBuilder.header("Cookie", cookieHeader(cookies));
    }
	//"User-Agent"通常須要做爲公共header外部統一添加
    if (userRequest.header("User-Agent") == null) {
      requestBuilder.header("User-Agent", Version.userAgent());
    }

	//處理請求
    Response networkResponse = chain.proceed(requestBuilder.build());
	//從networkResponse中獲取 header "Set-Cookie" 存入cookieJar
    HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());

    Response.Builder responseBuilder = networkResponse.newBuilder()
        .request(userRequest);

	//若是咱們沒有手動添加"Accept-Encoding: gzip",這裏會建立 能自動解壓的responseBody--GzipSource
    if (transparentGzip
        && "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
        && HttpHeaders.hasBody(networkResponse)) {
      GzipSource responseBody = new GzipSource(networkResponse.body().source());
      Headers strippedHeaders = networkResponse.headers().newBuilder()
          .removeAll("Content-Encoding")
          .removeAll("Content-Length")
          .build();
      responseBuilder.headers(strippedHeaders);
      String contentType = networkResponse.header("Content-Type");
      responseBuilder.body(new RealResponseBody(contentType, -1L, Okio.buffer(responseBody)));
    }
	//而後構建 新的response返回出去
    return responseBuilder.build();
  }
  
  private String cookieHeader(List<Cookie> cookies) {
    StringBuilder cookieHeader = new StringBuilder();
    for (int i = 0, size = cookies.size(); i < size; i++) {
      if (i > 0) {
        cookieHeader.append("; ");
      }
      Cookie cookie = cookies.get(i);
      cookieHeader.append(cookie.name()).append('=').append(cookie.value());
    }
    return cookieHeader.toString();
  }
}
複製代碼

首先,chain.proceed() 執行前,對 請求添加了header:"Content-Type"、"Content-Length" 或 "Transfer-Encoding"、"Host"、"Connection"、"Accept-Encoding"、"Cookie"、"User-Agent",即網絡層真正可執行的請求。其中,注意到,默認是沒有cookie處理的,須要咱們在初始化OkhttpClient時配置咱們本身的cookieJar

chain.proceed() 執行後,先把響應header中的cookie存入cookieJar(若是有),而後若是沒有手動添加請求header "Accept-Encoding: gzip",那麼會經過 建立能自動解壓的responseBody——GzipSource,接着構建新的response返回。

看起來BridgeInterceptor也是比較好理解的。

CacheInterceptor - 緩存攔截器

CacheInterceptor,緩存攔截器,提供網絡請求緩存的存取。 咱們發起一個網絡請求,若是每次都通過網絡的發送和讀取,那麼效率上是有欠缺的。若是以前有相同的請求已經執行過一次,那麼是否能夠把它的結果存起來,而後此次請求直接使用呢? CacheInterceptor乾的就是這個事,合理使用本地緩存,有效地減小網絡開銷、減小響應延遲。

在解析CacheInterceptor源碼前,先了解下http的緩存機制:

第一次請求:

第一次請求(圖片來源於網絡)
第二次請求:
第二次請求(圖片來源於網絡)
上面兩張圖很好的解釋了http的緩存機制:根據 緩存是否過時過時後是否有修改 來決定 請求是否使用緩存。詳細說明可點擊瞭解 完全弄懂HTTP緩存機制及原理

CacheInterceptor的intercept方法代碼以下:

public Response intercept(Chain chain) throws IOException {
  	//用reques的url 從緩存中 獲取響應 做爲候選(CacheStrategy決定是否使用)。
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;

    long now = System.currentTimeMillis();
	//根據 request、候選Response 獲取緩存策略。
	//緩存策略 將決定是否 使用緩存:strategy.networkRequest爲null,不使用網絡;strategy.cacheResponse爲null,不使用緩存。
    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();
    }

    // 若是不用網絡,cacheResponse確定不爲空了,那麼就使用緩存了,結束了。
    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }

	//到這裏,networkRequest != null (cacheResponse可能null,也可能!null)
	//networkRequest != null,就是要進行網絡請求了,因此 攔截器鏈 就繼續 往下處理了~
    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());
      }
    }

    // 若是網絡請求返回304,表示服務端資源沒有修改,那麼就 結合 網絡響應和緩存響應,而後更新緩存,返回,結束。
    if (cacheResponse != null) {
      if (networkResponse.code() == HTTP_NOT_MODIFIED) {
        Response response = cacheResponse.newBuilder()
            .headers(combine(cacheResponse.headers(), networkResponse.headers()))//結合header
            .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 {
      	//若是是非304,說明服務端資源有更新,就關閉緩存body
        closeQuietly(cacheResponse.body());
      }
    }

    Response response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();

    if (cache != null) {
      //網絡響應可緩存(請求和響應的 頭 Cache-Control都不是'no-store')
      if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
        // 寫入緩存
        CacheRequest cacheRequest = cache.put(response);
        return cacheWritingResponse(cacheRequest, response);
      }
      
	  //OkHttp默認只會對get請求進行緩存,由於get請求的數據通常是比較持久的,而post通常是交互操做,沒太大意義進行緩存
	  //不是get請求就移除緩存
      if (HttpMethod.invalidatesCache(networkRequest.method())) {
        try {
          cache.remove(networkRequest);
        } catch (IOException ignored) {
          // The cache cannot be written.
        }
      }
    }

    return response;
  }
複製代碼

總體思路很清晰:使用緩存策略CacheStrategy 來 決定是否使用緩存 及如何使用。

CacheStrategy是如何獲取的,這裏先不看。須要知道:strategy.networkRequest爲null,不使用網絡;strategy.cacheResponse爲null,不使用緩存

那麼按照這個思路,接下來就是一系列的判斷:

  1. 若 networkRequest、cacheResponse 都爲null,即網絡請求、緩存 都不能用,就返回504

  2. 若 networkRequest爲null,cacheResponse確定就不爲null,那麼就是 不用網絡,使用緩存了,就結束了

  3. 若 networkResponse不爲null,無論cacheResponse是否null,都會去請求網絡,獲取網絡響應networkResponse

  4. 接着3,若cacheResponse不爲null,且networkResponse.code是304,表示服務端資源未修改,緩存是還有效的,那麼結合 網絡響應和緩存響應,而後更新緩存,結束~

  5. 接着3,若cacheResponse爲null 或 cacheResponse不爲null但networkResponse.code不是304,那麼就寫入緩存,返回響應,結束~

繼續看,上面判斷邏輯都是基於CacheStrategy,這裏咱們就去看看它是如何生成的,看起來就是一行代碼:

CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
複製代碼

把請求request、候選緩存cacheCandidate傳入 工廠類Factory,而後調用get方法,那麼就看下吧:

public Factory(long nowMillis, Request request, Response cacheResponse) {
      this.nowMillis = nowMillis;
      this.request = request;
      this.cacheResponse = cacheResponse;

      if (cacheResponse != null) {
      	//獲取候選緩存的請求時間、響應時間,從header中獲取 過時時間、修改時間、資源標記等(若是有)。
        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);
          }
        }
      }
    }

    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;
    }
複製代碼

Factory的構造方法內,獲取了候選響應的請求時間、響應時間、過時時長、修改時間、資源標記等。

get方法內部先調用了getCandidate()獲取到緩存策略實例, 先跟進到 getCandidate方法看看吧:

private CacheStrategy getCandidate() {
      // 沒有緩存:網絡請求
      if (cacheResponse == null) {
        return new CacheStrategy(request, null);
      }

      // https,但沒有握手:網絡請求
      if (request.isHttps() && cacheResponse.handshake() == null) {
        return new CacheStrategy(request, null);
      }

      //網絡響應 不可緩存(請求或響應的 頭 Cache-Control 是'no-store'):網絡請求
      if (!isCacheable(cacheResponse, request)) {
        return new CacheStrategy(request, null);
      }
	  //請求頭的Cache-Control是no-cache 或者 請求頭有"If-Modified-Since"或"If-None-Match":網絡請求
	  //意思就是 不使用緩存 或者 請求 手動 添加了頭部 "If-Modified-Since"或"If-None-Match"
      CacheControl requestCaching = request.cacheControl();
      if (requestCaching.noCache() || hasConditions(request)) {
        return new CacheStrategy(request, null);
      }

      CacheControl responseCaching = cacheResponse.cacheControl();

	   //緩存的年齡
      long ageMillis = cacheResponseAge();
      //緩存的有效期
      long freshMillis = computeFreshnessLifetime();
	  //比較請求頭裏有效期,取較小值
      if (requestCaching.maxAgeSeconds() != -1) {
        freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
      }

	  //可接受的最小 剩餘有效時間(min-fresh標示了客戶端不肯意接受 剩餘有效期<=min-fresh 的緩存。)
      long minFreshMillis = 0;
      if (requestCaching.minFreshSeconds() != -1) {
        minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
      }
	  //可接受的最大過時時間(max-stale指令標示了客戶端願意接收一個已通過期了的緩存,例如 過時了 1小時 還能夠用)
      long maxStaleMillis = 0;
      if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
      		// 第一個判斷:是否要求必須去服務器驗證資源狀態
      		// 第二個判斷:獲取max-stale值,若是不等於-1,說明緩存過時後還能使用指定的時長
        maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
      }
      
	  //若是響應頭沒有要求忽略本地緩存 且 整合後的緩存年齡 小於 整合後的過時時間,那麼緩存就能夠用
      if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
        Response.Builder builder = cacheResponse.newBuilder();
        //沒有知足「可接受的最小 剩餘有效時間」,加個110警告
        if (ageMillis + minFreshMillis >= freshMillis) {
          builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
        }
        //isFreshnessLifetimeHeuristic表示沒有過時時間,那麼大於一天,就加個113警告
        long oneDayMillis = 24 * 60 * 60 * 1000L;
        if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
          builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
        }
        
        return new CacheStrategy(null, builder.build());
      }

      //到這裏,說明緩存是過時的
      // 而後 找緩存裏的Etag、lastModified、servedDate
      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();
          
      //conditionalRequest表示 條件網絡請求: 有緩存但過時了,去請求網絡 詢問服務端,還能不能用。能用側返回304,不能則正常執行網路請求。
      return new CacheStrategy(conditionalRequest, cacheResponse);
    }
複製代碼

一樣是進過一些列的判斷:

  1. 沒有緩存、是https但沒有握手、要求不可緩存、忽略緩存或手動配置緩存過時,都是 直接進行 網絡請求。
  2. 以上都不知足時,若是緩存沒過時,那麼就是用緩存(可能要添加警告)。
  3. 若是緩存過時了,但響應頭有Etag,Last-Modified,Date,就添加這些header 進行條件網絡請求
  4. 若是緩存過時了,且響應頭沒有設置Etag,Last-Modified,Date,就進行網絡請求。

接着回頭看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;
    }
複製代碼

getCandidate()獲取的緩存策略候選後,又進行了一個判斷:使用網絡請求 可是 原請求配置了只能使用緩存,按照上面的分析,此時即便有緩存,也是過時的緩存,因此又new了實例,兩個值都爲null。

好了,到這裏okhttp的緩存機制源碼就看完了。你會發現,okhttp的緩存機制 是符合 開頭 http的緩存機制 那兩張圖的,只是增長不少細節判斷。

另外,注意到,緩存的讀寫是經過 InternalCache完成的。InternalCache是在建立CacheInterceptor實例時 用client.internalCache()做爲參數傳入。而InternalCache是okhttp內部使用,相似一個代理,InternalCache的實例是 類Cache的屬性。Cache是咱們初始化OkHttpClient時傳入的。因此若是沒有傳入Cache實例是沒有緩存功能的。

OkHttpClient client = new OkHttpClient.Builder()
                .cache(new Cache(getExternalCacheDir(),500 * 1024 * 1024))
                .build();
複製代碼

緩存的增刪改查,Cache是經過okhttp內部的DiskLruCache實現的,原理和jakewharton的DiskLruCache是一致的,這裏就再也不敘述,這不是本文重點。

總結

好了,這篇文章到這裏內容講完了,仍是挺豐富的。講解了三個攔截器的工做原理:RetryAndFollowUpInterceptor、BridgeInterceptor、CacheInterceptor,其中CacheInterceptor是請求緩存處理,是比較重要的知識點。目前爲止,尚未看到真正的網絡鏈接和請求響應讀寫,這些內容會在下一篇講解 — 剩下的兩個攔截器 :ConnectInterceptor、CallServerInterceptor。(否則就太長了😁)

歡迎繼續關注,感謝!

.

你的 點贊、評論、收藏、轉發,是對個人巨大鼓勵!

.

歡迎關注個人公衆號

微信公衆號
相關文章
相關標籤/搜索