OkHttp源碼分析(二)——攔截器鏈

本片文章主要分析的是OkHttp獲取響應(yīng)的過程赃泡,以及攔截器鏈税灌。

getResponseWithInterceptorChain方法

在上篇分析同步和異步請(qǐng)求流程的時(shí)候都出現(xiàn)了getResponseWithInterceptorChain方法,現(xiàn)在從這里開始分析螟凭。

Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    //添加應(yīng)用攔截器
    interceptors.addAll(client.interceptors());
    //添加重試和重定向攔截器
    interceptors.add(new RetryAndFollowUpInterceptor(client));
    //添加轉(zhuǎn)換攔截器
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    //添加緩存攔截器
    interceptors.add(new CacheInterceptor(client.internalCache()));
    //添加連接攔截器
    interceptors.add(new ConnectInterceptor(client));
    //添加網(wǎng)絡(luò)攔截器
    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());
    
    boolean calledNoMoreExchanges = false;
    try {
      Response response = chain.proceed(originalRequest);
      if (transmitter.isCanceled()) {
        closeQuietly(response);
        throw new IOException("Canceled");
      }
      return response;
    } catch (IOException e) {
      calledNoMoreExchanges = true;
      throw transmitter.noMoreExchanges(e);
    } finally {
      if (!calledNoMoreExchanges) {
        transmitter.noMoreExchanges(null);
      }
    }
}

這段代碼主要是把

  • 應(yīng)用攔截器(外部配置)client.interceptors()
  • 重試跟進(jìn)攔截器RetryAndFollowUpInterceptor
  • 橋攔截器BridgetInterceptor
  • 緩存攔截器CacheInterceptor
  • 連接攔截器ConnectInterceptor
  • 網(wǎng)絡(luò)攔截器(外部配置)client.neworkInterceptors()
  • 請(qǐng)求服務(wù)攔截器CallServerInterceptor

將這些攔截器一次添加到集合interceptors中虚青,然后使用interceptors、transmitter螺男、originalRequest等創(chuàng)建了攔截器鏈RealInterceptorChain實(shí)例棒厘,最后用proceed方法獲取到請(qǐng)求的結(jié)果Response。

RealInterceptorChain

public Response proceed(Request request, Transmitter transmitter, @Nullable Exchange exchange)
  throws IOException {
    if (index >= interceptors.size()) throw new AssertionError();
    
    calls++;
    
    if (this.exchange != null && !this.exchange.connection().supportsUrl(request.url())) {
      throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
          + " must retain the same host and port");
    }
    
    // If we already have a stream, confirm that this is the only call to chain.proceed().
    if (this.exchange != null && calls > 1) {
      throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
          + " must call proceed() exactly once");
    }
    
    // Call the next interceptor in the chain.
    RealInterceptorChain next = new RealInterceptorChain(interceptors, transmitter, exchange,
        index + 1, request, call, connectTimeout, readTimeout, writeTimeout);
    Interceptor interceptor = interceptors.get(index);
    Response response = interceptor.intercept(next);
    
    // Confirm that the next interceptor made its required call to chain.proceed().
    if (exchange != null && index + 1 < interceptors.size() && next.calls != 1) {
      throw new IllegalStateException("network interceptor " + interceptor
          + " must call proceed() exactly once");
    }
    
    // Confirm that the intercepted response isn't null.
    if (response == null) {
      throw new NullPointerException("interceptor " + interceptor + " returned null");
    }
    
    if (response.body() == null) {
      throw new IllegalStateException(
          "interceptor " + interceptor + " returned a response with no body");
    }

return response;
}

在實(shí)例化RealInterceptorChain時(shí) index賦值是0下隧,exchange是null奢人,所以前面三個(gè)if都沒走進(jìn)去。然后獲取了第一個(gè)攔截器淆院,也就是我們配置的應(yīng)用攔截器邻邮,調(diào)用了它的interceptor方法嗜桌,并返回和校驗(yàn)了結(jié)果骡送。這里證實(shí)了我們猜想蛛株。同時(shí)注意到,調(diào)用 應(yīng)用攔截器的interceptor方法傳入的參數(shù):攔截器鏈實(shí)例next,next就是把index + 1而已,其他參數(shù)和當(dāng)前實(shí)例是一樣的。也就是說 在我們的應(yīng)用攔截器中調(diào)用的是 next的proceed方法指孤。

進(jìn)一步,next的proceed方法中 同樣會(huì)獲取interceptors的index=1的攔截器贬堵,即RetryAndFollowUpInterceptor實(shí)例恃轩,然后調(diào)用其interceptor方法,參數(shù)是index+1即index=2的chain黎做。跟進(jìn)RetryAndFollowUpInterceptor的代碼發(fā)現(xiàn)详恼,interceptor方法內(nèi)部也是有調(diào)用chain的proceed方法。這樣就會(huì)依次傳遞下去引几,直到最后一個(gè)攔截器CallServerInterceptor昧互。

實(shí)際上 除了最后一個(gè)攔截器CallServerInterceptor之外,所有攔截器的interceptor方法都調(diào)用了 傳入 chain的proceed方法伟桅。每個(gè)攔截器在chain的proceed方法 前后 處理了自己負(fù)責(zé)的工作敞掘。例如我們的應(yīng)用攔截器,在chain的proceed方法前 打印了request信息的日志楣铁,chain的proceed方法獲取結(jié)果 之后 打印了response信息的日志玖雁。每個(gè)攔截器interceptor方法在 調(diào)用chain的proceed方法時(shí) 都是為了獲取下一個(gè)攔截器處理的response,然后返回給上一個(gè)攔截器盖腕。

下面我們依次分析這些攔截器赫冬。

RetryAndFollowUpInterceptor-重試、重定向

如果請(qǐng)求創(chuàng)建時(shí)沒有添加應(yīng)用攔截器 溃列,那第一個(gè)攔截器就是RetryAndFollowInterceptor,意為重試和跟進(jìn)攔截器劲厌。作用是連接失敗后進(jìn)行重試,對(duì)請(qǐng)求結(jié)果跟進(jìn)后進(jìn)行重定向听隐。下面看下它的interceptor方法:

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) {
      //準(zhǔn)備連接
      transmitter.prepareToConnect(request);
    
      if (transmitter.isCanceled()) {
        throw new IOException("Canceled");
      }
    
      Response response;
      boolean success = false;
      try {
        //繼續(xù)執(zhí)行下一個(gè)Interceptor
        response = realChain.proceed(request, transmitter, null);
        success = true;
      } catch (RouteException e) {
        // 連接路由異常补鼻,此時(shí)請(qǐng)求還未發(fā)送,嘗試恢復(fù)
        if (!recover(e.getLastConnectException(), transmitter, false, request)) {
          throw e.getFirstConnectException();
        }
        continue;
      } catch (IOException e) {
        // IO異常 請(qǐng)求可能已發(fā)出雅任,嘗試恢復(fù)
        if (!recover(e, transmitter, requestSendStarted, request)) throw e;
        continue;
      } finally {
        // 請(qǐng)求沒成功风范,釋放資源
        if (!success) {
          transmitter.exchangeDoneDueToException();
        }
      }
    
      // 關(guān)聯(lián)上一個(gè)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;
      //跟進(jìn)結(jié)果,主要作用是根據(jù)響應(yīng)碼處理請(qǐng)求沪么,返回request不為空時(shí)則進(jìn)行重定向處理硼婿,拿到重定向的request
      Request followUp = followUpRequest(response, route);
    
      if (followUp == null) {
        if (exchange != null && exchange.isDuplex()) {
          transmitter.timeoutEarlyExit();
        }
        return response;
      }
    
      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循環(huán)

prepareToConnect

public void prepareToConnect(Request request) {
    if (this.request != null) {
      if (sameConnection(this.request.url(), request.url()) && exchangeFinder.hasRouteToTry()) {
        return; // 有相同連接
      }
      ...
    }
    
    this.request = request;
    //創(chuàng)建ExchangeFinder,是為獲取連接做準(zhǔn)備
    this.exchangeFinder = new ExchangeFinder(this, connectionPool, createAddress(request.url()),
        call, eventListener);
}

ExchangeFinder是交換查找器禽车,作用是獲取請(qǐng)求的連接寇漫。

接著調(diào)用realChain.proceed繼續(xù)傳遞請(qǐng)求給下一個(gè)攔截器拳喻,從下個(gè)攔截器獲取原始結(jié)果。如果此過程發(fā)生了連接路由異持硗螅或IO異常,就會(huì)調(diào)用recover判斷是否進(jìn)行重試恢復(fù)钦勘。

recover

private boolean recover(IOException e, Transmitter transmitter,
      boolean requestSendStarted, Request userRequest) {
    // 應(yīng)用層禁止重試陋葡,就不重試
    if (!client.retryOnConnectionFailure()) return false;
    
    // 不能再次發(fā)送請(qǐng)求,就不重試
    if (requestSendStarted && requestIsOneShot(e, userRequest)) return false;
    
    // 發(fā)生的異常是致命的彻采,就不重試
    if (!isRecoverable(e, requestSendStarted)) return false;
    
    // 沒有路由可以嘗試腐缤,就不重試
    if (!transmitter.canRetry()) return false;
    
    //返回true,就會(huì)進(jìn)入下一次循環(huán)肛响,重新請(qǐng)求
    return true;
}

如果realChain.proceed沒有發(fā)生異常岭粤,返回了結(jié)果response,就會(huì)使用followUpRequest方法跟進(jìn)結(jié)果并重定向request。如果不用跟進(jìn)處理(例如響應(yīng)碼是200)特笋,則返回null剃浇。

BridgeInterceptor-橋接攔截器

橋攔截器相當(dāng)于在請(qǐng)求發(fā)起端和網(wǎng)絡(luò)執(zhí)行端架起一座橋,其作用:

  • 把應(yīng)用層發(fā)出的請(qǐng)求變?yōu)榫W(wǎng)絡(luò)層認(rèn)識(shí)的請(qǐng)求猎物;
  • 把網(wǎng)絡(luò)層執(zhí)行后的響應(yīng)變?yōu)閼?yīng)用層便于應(yīng)用層使用的結(jié)果虎囚。
public final class BridgeInterceptor implements
Interceptor {
  //cookie管理器,初始化OkHttpClient時(shí)創(chuàng)建的蔫磨,默認(rèn)是C
  private final CookieJar cookieJar;

  public BridgeInterceptor(CookieJar cookieJar) {
    this.cookieJar = cookieJar;
  }
  ...//這部分代碼比較長(zhǎng)我們?cè)谙旅娣植秸归_
}

添加頭部信息

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();
      if (contentLength != -1) {
        requestBuilder.header("Content-Length", Long.toString(contentLength));
        requestBuilder.removeHeader("Transfer-Encoding");
      } else {
        requestBuilder.header("Transfer-Encoding", "chunked");
        requestBuilder.removeHeader("Content-Length");
      }
}

這段代碼主要為request添加Content-Type(文檔類型)淘讥、Content-Length(內(nèi)容長(zhǎng)度)或Transfer-Encoding。這些信息不需要我們手動(dòng)添加堤如。

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

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

//默認(rèn)支持gzip壓縮
//"Accept-Encoding: gzip",表示接受:返回gzip編碼壓縮的數(shù)據(jù)
// 如果我們手動(dòng)添加了 "Accept-Encoding: gzip" 蒲列,那么下面的if不會(huì)進(jìn)入,transparentGzip是false搀罢,就需要我們自己處理數(shù)據(jù)解壓蝗岖。
//如果 沒有 手動(dòng)添加"Accept-Encoding: gzip" ,transparentGzip是true榔至,同時(shí)會(huì)自動(dòng)添加剪侮,而且后面也會(huì)自動(dòng)處理解壓。
boolean transparentGzip = false;
boolean transparentGzip = false;
if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
  transparentGzip = true;
  requestBuilder.header("Accept-Encoding", "gzip");
}

if (userRequest.header("User-Agent") == null) {
    requestBuilder.header("User-Agent", Version.userAgent());
}

這段代碼主要為Host洛退、Connection和User-Agent字段添加默認(rèn)值瓣俯。這些屬性只有用戶沒有設(shè)置時(shí),才會(huì)自動(dòng)添加兵怯。

cookie部分

//從cookieJar中獲取cookie,添加到Header
List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
if (!cookies.isEmpty()) {
  requestBuilder.header("Cookie", cookieHeader(cookies));
}

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

處理請(qǐng)求

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

//如果我們手動(dòng)添加“Accept-Encoding:gzip”,這里會(huì)創(chuàng)建  能自動(dòng)解壓的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)));
    }

梳理下整個(gè)流程:

  • chain.proceed()執(zhí)行前彩匕,對(duì)請(qǐng)求添加Header:Content-Type、Content-Length或Transfer-Encoding媒区、Host驼仪、Connection掸犬、Accept-Encoding、Cookie绪爸、User-Agent,即網(wǎng)絡(luò)層真正可執(zhí)行的請(qǐng)求湾碎。默認(rèn)是沒有cookie處理的,需要我們?cè)诔跏蓟疧kHttpClient時(shí)配置我們自己的cookieJar
  • chain.proceed()執(zhí)行后奠货,先把響應(yīng)header中的cookie存入cookieJar介褥,如果沒有手動(dòng)添加請(qǐng)求heade:"Accept-Encoding:gzip",會(huì)通過創(chuàng)建能自動(dòng)解壓的responseBody——GzipSource递惋,接著構(gòu)建新的response返回柔滔。

CacheInterceptor-緩存攔截器

緩存攔截器,提供網(wǎng)絡(luò)請(qǐng)求緩存的存取萍虽。

發(fā)送一個(gè)網(wǎng)絡(luò)請(qǐng)求睛廊,如果每次都經(jīng)過網(wǎng)絡(luò)的發(fā)送和讀取,效率肯定是很低的杉编。若之前有相同的請(qǐng)求已經(jīng)執(zhí)行過一次超全,是否可以將其結(jié)果保存起來,這次請(qǐng)求直接使用邓馒。這就用到了CacheInterceptor卵迂,合理使用本地緩存,有效的減少網(wǎng)絡(luò)開銷绒净,減少響應(yīng)延遲见咒。

  final @Nullable InternalCache cache;

  public CacheInterceptor(@Nullable InternalCache cache) {
    this.cache = cache;
  }
  
  @Override 
  public Response intercept(Chain chain) throws IOException {
    // 先從緩存中獲取響應(yīng),沒有則返回null挂疆。
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;

    long now = System.currentTimeMillis();
    
    //獲取CacheStrategy緩存策略
    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    Request networkRequest = strategy.networkRequest;
    Response cacheResponse = strategy.cacheResponse;
    
    //根據(jù)緩存策略更新統(tǒng)計(jì)指標(biāo):請(qǐng)求次數(shù)改览、網(wǎng)絡(luò)請(qǐng)求次數(shù)、使用緩存次數(shù)
    if (cache != null) {
      cache.trackResponse(strategy);
    }
    
    //有緩存 但不能用 關(guān)掉
    if (cacheCandidate != null && cacheResponse == null) {
      closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
    }

    // 網(wǎng)絡(luò)請(qǐng)求 緩存都不能用缤言,返回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();
    }

    // 不需要網(wǎng)絡(luò)請(qǐng)求宝当,可以使用緩存,就不會(huì)再走后面的流程
    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }
    
    // 進(jìn)行網(wǎng)絡(luò)請(qǐng)求
    Response networkResponse = null;
    try {
      //調(diào)用下一個(gè)攔截器
      networkResponse = chain.proceed(networkRequest);
    } finally {
      // 發(fā)生IO錯(cuò)誤
      if (networkResponse == null && cacheCandidate != null) {
        closeQuietly(cacheCandidate.body());
      }
    }

    //網(wǎng)路請(qǐng)求返回304胆萧,表示服務(wù)端資源沒有修改庆揩,就結(jié)合網(wǎng)絡(luò)響應(yīng)和網(wǎng)絡(luò)緩存,更新緩存跌穗,返回結(jié)果订晌,結(jié)束。
    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();

        cache.trackConditionalCacheHit();
        cache.update(cacheResponse, response);
        return response;
      } else {
        //如果是非304蚌吸,說明服務(wù)端資源有更新锈拨,就關(guān)閉緩存body
        closeQuietly(cacheResponse.body());
      }
    }

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

    if (cache != null) {
      //如果有響應(yīng)體并且可緩存,那么將響應(yīng)寫入緩存
      if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
        // 寫入緩存
        CacheRequest cacheRequest = cache.put(response);
        return cacheWritingResponse(cacheRequest, response);
      }
      
      // OkHttp默認(rèn)只會(huì)對(duì)get請(qǐng)求進(jìn)行緩存 因?yàn)間et請(qǐng)求的數(shù)據(jù)一般是比較持久的 而post一般是交互操作
      //不是get請(qǐng)求就移除緩存
      if (HttpMethod.invalidatesCache(networkRequest.method())) {
        try {
          cache.remove(networkRequest);
        } catch (IOException ignored) {
          // The cache cannot be written.
        }
      }
    }

    return response;
  }

現(xiàn)在來整體梳理下思路羹唠。

CacheStrategy緩存策略用來決定是否使用緩存及如何使用奕枢。根據(jù)緩存策略中的networkRequest和cacheResponse來進(jìn)行一系列是使用緩存還是新的網(wǎng)絡(luò)數(shù)據(jù)的判斷:

  1. 若networkRequest娄昆、cacheResponse都為null,即網(wǎng)絡(luò)請(qǐng)求、緩存都不能用缝彬,返回504萌焰;
  2. 若networkRequest為null,cacheResponse肯定不為null,就是不使用網(wǎng)絡(luò)谷浅,使用緩存扒俯,就結(jié)束返回緩存數(shù)據(jù);
  3. 若networkResponse不為null壳贪,不管cacheResponse是否為null,都會(huì)去請(qǐng)求網(wǎng)絡(luò),獲取網(wǎng)絡(luò)響應(yīng)networkResponse;
  4. 若cacheResponse不為null寝杖,且networkResponse.code是304违施,表示服務(wù)端資源未修改,緩存還是有效的瑟幕。結(jié)合網(wǎng)絡(luò)響應(yīng)和緩存響應(yīng)磕蒲,然后更新緩存;
  5. 若cacheResponse==null或cacheResponse不為null,但networkResponse.code不是304只盹,就寫入緩存辣往,返回響應(yīng)。

CacheStrategy

CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();

這里將請(qǐng)求request殖卑、候選緩存cacheCandidate傳入工廠類Factory站削,然后調(diào)用get方法。

public Factory(long nowMillis, Request request, Response cacheResponse) {
  this.nowMillis = nowMillis;
  this.request = request;
  this.cacheResponse = cacheResponse;
  
  //獲取候選緩存的請(qǐng)求時(shí)間孵稽、響應(yīng)時(shí)間许起,從header中獲取過期時(shí)間、修改時(shí)間菩鲜、資源標(biāo)記等园细。
  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);
      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);
      }
    }
  }
}

//get方法內(nèi)部先調(diào)用了getCandidate()獲取到緩存策略實(shí)例
public CacheStrategy get() {
  CacheStrategy candidate = getCandidate();

  if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
    return new CacheStrategy(null, null);
  }

  return candidate;
}

getCandidate()

這個(gè)方法里涉及到很多http緩存字段方面的東西

    private CacheStrategy getCandidate() {
      // 沒有緩存:網(wǎng)絡(luò)請(qǐng)求
      if (cacheResponse == null) {
        return new CacheStrategy(request, null);
      }

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

      //網(wǎng)絡(luò)響應(yīng) 不可緩存(請(qǐng)求或響應(yīng)的 頭 Cache-Control 是'no-store'):網(wǎng)絡(luò)請(qǐng)求
      if (!isCacheable(cacheResponse, request)) {
        return new CacheStrategy(request, null);
      }
      //請(qǐng)求頭的Cache-Control是no-cache 或者 請(qǐng)求頭有"If-Modified-Since"或"If-None-Match":網(wǎng)絡(luò)請(qǐng)求
      //意思就是 不使用緩存 或者 請(qǐng)求 手動(dòng) 添加了頭部 "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();
      //比較請(qǐng)求頭里有效期接校,取較小值
      if (requestCaching.maxAgeSeconds() != -1) {
        freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
      }

      //可接受的最小 剩余有效時(shí)間(min-fresh標(biāo)示了客戶端不愿意接受 剩余有效期<=min-fresh 的緩存猛频。)
      long minFreshMillis = 0;
      if (requestCaching.minFreshSeconds() != -1) {
        minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
      }
      //可接受的最大過期時(shí)間(max-stale指令標(biāo)示了客戶端愿意接收一個(gè)已經(jīng)過期了的緩存,例如 過期了 1小時(shí) 還可以用)
      long maxStaleMillis = 0;
      if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
            // 第一個(gè)判斷:是否要求必須去服務(wù)器驗(yàn)證資源狀態(tài)
            // 第二個(gè)判斷:獲取max-stale值蛛勉,如果不等于-1鹿寻,說明緩存過期后還能使用指定的時(shí)長(zhǎng)
        maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
      }
      
      //如果響應(yīng)頭沒有要求忽略本地緩存 且 整合后的緩存年齡 小于 整合后的過期時(shí)間,那么緩存就可以用
      if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
        Response.Builder builder = cacheResponse.newBuilder();
        //沒有滿足“可接受的最小 剩余有效時(shí)間”诽凌,加個(gè)110警告
        if (ageMillis + minFreshMillis >= freshMillis) {
          builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
        }
        //isFreshnessLifetimeHeuristic表示沒有過期時(shí)間烈和,那么大于一天,就加個(gè)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 {
        //都沒有,就執(zhí)行常規(guī)的網(wǎng)絡(luò)請(qǐng)求
        return new CacheStrategy(request, null); // No condition! Make a regular request.
      }

      //如果有疯暑,就添加到網(wǎng)絡(luò)請(qǐng)求的頭部训柴。
      Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();
      Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);

      Request conditionalRequest = request.newBuilder()
          .headers(conditionalRequestHeaders.build())
          .build();
          
      //conditionalRequest表示 條件網(wǎng)絡(luò)請(qǐng)求: 有緩存但過期了,去請(qǐng)求網(wǎng)絡(luò) 詢問服務(wù)端妇拯,還能不能用幻馁。能用側(cè)返回304,不能則正常執(zhí)行網(wǎng)路請(qǐng)求越锈。
      return new CacheStrategy(conditionalRequest, cacheResponse);
    }

下面總結(jié)下getCandidate()方法的流程:

  1. 沒有緩存仗嗦、是https請(qǐng)求但是沒有握手、網(wǎng)絡(luò)響應(yīng)不可緩存甘凭、忽略緩存或手動(dòng)配置緩存過期稀拐,都直接進(jìn)行網(wǎng)絡(luò)請(qǐng)求;
  2. 以上條件都不滿足丹弱,如果緩存沒過期那么就使用緩存;
  3. 如果緩存過期了德撬,但響應(yīng)頭有Etag、Last-Modified躲胳、Data蜓洪,就添加這些header進(jìn)行條件網(wǎng)絡(luò)請(qǐng)求;
  4. 如果緩存過期了,且響應(yīng)頭沒有設(shè)置Etag坯苹、Last-Modified隆檀、Data,就進(jìn)行網(wǎng)絡(luò)請(qǐng)求粹湃。

再繼續(xù)看get()方法:

public CacheStrategy get() {
      CacheStrategy candidate = getCandidate();
      if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
        return new CacheStrategy(null, null);
      }
      return candidate;
    }

getCandidate()獲取的緩存策略對(duì)象后刚操,判斷:進(jìn)行了網(wǎng)絡(luò)請(qǐng)求且原請(qǐng)求配置是能使用緩存。這說明此時(shí)即使有緩存也是過期的緩存再芋,所以new一個(gè)實(shí)例菊霜,傳入null。

緩存的讀寫是通過InternalCache完成的济赎,InternalCache是在創(chuàng)建CacheInterceptor實(shí)例時(shí)鉴逞,用client.internalCache()作為參數(shù)傳入。而InternalCahce是OkHttp內(nèi)部使用司训,InternalCache的實(shí)例是類Cache的屬性构捡。Cache是我們初始化OkHttpClient時(shí)傳入的,所以如果沒有傳入Cache實(shí)例是沒有緩存功能的壳猜。

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

Cache是通過OkHttp內(nèi)部的DiskLruCache實(shí)現(xiàn)的勾徽。

ConnectInterceptor攔截器和CallServerInterceptor攔截器會(huì)下下一篇文章中分析。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末统扳,一起剝皮案震驚了整個(gè)濱河市喘帚,隨后出現(xiàn)的幾起案子畅姊,更是在濱河造成了極大的恐慌,老刑警劉巖吹由,帶你破解...
    沈念sama閱讀 217,406評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件若未,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡倾鲫,警方通過查閱死者的電腦和手機(jī)粗合,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來乌昔,“玉大人隙疚,你說我怎么就攤上這事】牡溃” “怎么了供屉?”我有些...
    開封第一講書人閱讀 163,711評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)捅厂。 經(jīng)常有香客問我贯卦,道長(zhǎng)资柔,這世上最難降的妖魔是什么焙贷? 我笑而不...
    開封第一講書人閱讀 58,380評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮贿堰,結(jié)果婚禮上辙芍,老公的妹妹穿的比我還像新娘。我一直安慰自己羹与,他們只是感情好故硅,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著纵搁,像睡著了一般吃衅。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上腾誉,一...
    開封第一講書人閱讀 51,301評(píng)論 1 301
  • 那天徘层,我揣著相機(jī)與錄音,去河邊找鬼利职。 笑死趣效,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的猪贪。 我是一名探鬼主播跷敬,決...
    沈念sama閱讀 40,145評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼热押!你這毒婦竟也來了西傀?” 一聲冷哼從身側(cè)響起斤寇,我...
    開封第一講書人閱讀 39,008評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎池凄,沒想到半個(gè)月后抡驼,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,443評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡肿仑,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評(píng)論 3 334
  • 正文 我和宋清朗相戀三年致盟,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片尤慰。...
    茶點(diǎn)故事閱讀 39,795評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡馏锡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出伟端,到底是詐尸還是另有隱情杯道,我是刑警寧澤,帶...
    沈念sama閱讀 35,501評(píng)論 5 345
  • 正文 年R本政府宣布责蝠,位于F島的核電站党巾,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏霜医。R本人自食惡果不足惜齿拂,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望肴敛。 院中可真熱鬧署海,春花似錦、人聲如沸医男。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽镀梭。三九已至刀森,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間报账,已是汗流浹背研底。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留笙什,地道東北人飘哨。 一個(gè)月前我還...
    沈念sama閱讀 47,899評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像琐凭,于是被迫代替她去往敵國(guó)和親芽隆。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評(píng)論 2 354