Android深入理解源碼——OkHttp篇(下)

聲明:原創(chuàng)作品,轉載請注明出處http://www.reibang.com/p/8c32e928613c

這篇文章接著上面一篇文章來詳細分析各個攔截器的實現(xiàn)機制菩帝,主要的攔截器有這幾個:

  • RetryAndFollowUpInterceptor
  • BridgeInterceptor朝抖、
  • CacheInterceptor崭庸、
  • ConnectInterceptor邢锯、
  • CallServerInterceptor扬蕊。

接下來挨個看下:

1.RetryAndFollowUpInterceptor

這個攔截器是用來處理異常請求重試和重定向的,所謂重定向丹擎,說的簡單點就是請求某一個資源尾抑,被告知資源被更改,讓你換個路徑重新請求蒂培。接下來就來看下源碼是怎么實現(xiàn)的:


  @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 {
        response = realChain.proceed(request, transmitter, null);
        success = true;
      } catch (RouteException e) {
        // The attempt to connect via a route failed. The request will not have been sent.
        if (!recover(e.getLastConnectException(), transmitter, false, request)) {
          throw e.getFirstConnectException();
        }
        continue;
      } catch (IOException e) {
        // An attempt to communicate with a server failed. The request may have been sent.
        boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
        if (!recover(e, transmitter, requestSendStarted, request)) throw e;
        continue;
      } finally {
        // The network call threw an exception. Release any resources.
        if (!success) {
          transmitter.exchangeDoneDueToException();
        }
      }

      // Attach the prior response if it exists. Such responses never have a body.
      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 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();
      }

      if (++followUpCount > MAX_FOLLOW_UPS) {
        throw new ProtocolException("Too many follow-up requests: " + followUpCount);
      }

      request = followUp;
      priorResponse = response;
    }
  }

源碼有點長再愈,我們來挨個看下,首先拿到請求體request护戳、這個鏈條chain以及transmitter翎冲,這個transmitter其實是應用層和網(wǎng)絡層的一個橋梁。接下來會進入到一個while循環(huán)中媳荒,在循環(huán)一開始會調用transmitter的prepareToConnect方法進行網(wǎng)絡層的初始化,如下代碼抗悍,然后判斷下該請求是否已被取消,如果是則直接拋出異常钳枕。

transmitter.prepareToConnect(request);
if (transmitter.isCanceled()) {
  throw new IOException("Canceled");
}

接下來就是調用chain的proceed方法將request傳遞給下一個攔截器進行網(wǎng)絡請求缴渊,如下:

      Response response;
      boolean success = false;
      try {
        response = realChain.proceed(request, transmitter, null);
        success = true;
      } catch (RouteException e) {
        // The attempt to connect via a route failed. The request will not have been sent.
        if (!recover(e.getLastConnectException(), transmitter, false, request)) {
          throw e.getFirstConnectException();
        }
        continue;
      } catch (IOException e) {
        // An attempt to communicate with a server failed. The request may have been sent.
        boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
        if (!recover(e, transmitter, requestSendStarted, request)) throw e;
        continue;
      } finally {
        // The network call threw an exception. Release any resources.
        if (!success) {
          transmitter.exchangeDoneDueToException();
        }
      }

這個proceed方法在一個try/catch中執(zhí)行,當出現(xiàn)對應的異常時鱼炒,會調用recover方法來判斷這個請求是否是可恢復的衔沼,如果可恢復則會重新執(zhí)行while循環(huán)進行請求重試。如果不可恢復則直接拋出對應的異常昔瞧。
我們來看下recover的判斷邏輯:

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

    // 無法再次發(fā)送請求體
    if (requestSendStarted && requestIsOneShot(e, userRequest)) return false;

    // 發(fā)生嚴重的錯誤異常
    if (!isRecoverable(e, requestSendStarted)) return false;

    // 沒有額外的路由可嘗試
    if (!transmitter.canRetry()) return false;

    return true;
  }

首先會判斷下我們的client是否配置了當連接失敗可以重試指蚁,如果沒有則返回false,即不可恢復硬爆。如果我們配置了可以重試,那么接下來會判斷我們的請求是否已經(jīng)發(fā)送出去擎鸠,并且請求只能被發(fā)送一次缀磕,如果滿足條件則表示不可恢復。如果不滿足條件,則會調用isRecoverable方法進行接下來的判斷袜蚕。這個方法會判斷拋出的異常是什么異常糟把,如果是協(xié)議異常或者其他的一些特殊的異常則不可恢復牲剃。否則就調用transmitter的canRetry()方法進行判斷遣疯,這個方法內部會判斷是否有更多的路由可重試,如果沒有則返回false不可重試凿傅,如果上面的條件都不滿足則返回true缠犀,表示可重試。
接下來我們跳出recover方法聪舒,繼續(xù)看接下來的代碼辨液。
如果上面的執(zhí)行沒有拋出異常,則會繼續(xù)往下接著執(zhí)行:

      // Attach the prior response if it exists. Such responses never have a body.
      if (priorResponse != null) {
        response = response.newBuilder()
            .priorResponse(priorResponse.newBuilder()
                    .body(null)
                    .build())
            .build();
      }

接下來會判斷下priorResponse是否為空箱残,這個priorResponse是保存著上次返回的response滔迈,如果不為空則會創(chuàng)建一個新的response,這個新的response將老的response和當前的response組合起來被辑。這里我們是第一次執(zhí)行燎悍,所以priorResponse為空,里面也就不會執(zhí)行盼理。接下來再看下面的代碼:


      Exchange exchange = Internal.instance.exchange(response);
      Route route = exchange != null ? exchange.connection().route() : null;
      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();
      }

      if (++followUpCount > MAX_FOLLOW_UPS) {
        throw new ProtocolException("Too many follow-up requests: " + followUpCount);
      }

      request = followUp;
      priorResponse = response;

首先會通過response得到exchange和route谈山,exchange可以理解成實際處理io的類。然后調用followUpRequest方法并傳入exchange和route參數(shù)獲取重定向后的request榜揖,當然如果不是重定向的話就會返回空勾哩。接下來,如果上面返回的followUp重定向request為空的話举哟,則表示我們的請求是正常的思劳,就直接返回。這樣到這邏輯就執(zhí)行結束了妨猩。如果不為空就會接著往下執(zhí)行潜叛,如果followUp的body不為空并且只能被發(fā)送一次,那么就直接返回這個response壶硅,執(zhí)行結束威兜。當然這個isOneShot方法默認是false的,所以不會直接返回庐椒。接下來繼續(xù)執(zhí)行椒舵,會關閉一些資源,然后把上面的followUp重定向的request作為新的request约谈,然后把重定向返回的response賦值給priorResponse笔宿,接著會重復while循環(huán)進行再次的網(wǎng)絡請求犁钟。當然這里有個判斷重定向次數(shù)的邏輯,如果重定向超出20次則會拋出異常泼橘。
這樣我們的RetryAndFollowUpInterceptor攔截器就分析完了涝动。

2.BridgeInterceptor

接下來看下BridgeInterceptor攔截器,這個攔截器顧名思義是起到一個橋梁的作用炬灭,連接應用層和網(wǎng)絡層的代碼醋粟。相當于把應用層的代碼轉換成比較繁瑣的HTTP協(xié)議相關的東西,比如報文頭部的一些字段重归。比較簡單這里就不展開說了米愿。

3.CacheInterceptor

接下來看下CacheInterceptor這個攔截器,看名字就可以看出來這個是用來緩沖的提前,緩存HTTP返回的數(shù)據(jù)吗货。講這個緩存攔截器之前還是有必要講下HTTP的緩存機制。

HTTP緩存機制

我們知道一個HTTP請求狈网,其實就是客戶端發(fā)送請求報文宙搬,然后服務器接返回響應報文的過程。通常當我們需要某個資源的時候我們就會直接從服務器那請求拓哺,但如果每次請求時服務器資源都是一樣的沒有發(fā)生改變勇垛,這時我們就可以在第一次拿到資源后存在本地,下次如果需要就直接從本地讀取士鸥。但是有個問題闲孤,什么時候從本地獲取什么時候從服務器拉取。這就涉及到HTTP的緩存機制烤礁。
HTTP緩存機制聽起來挺復雜讼积,其實就是利用一些HTTP報文頭來定義一套緩存規(guī)則。
HTTP緩存分強制緩存對比緩存脚仔。

強制緩存

強制緩存

如上圖所示勤众,左右兩個圖分別是本地緩存命中和不命中的流程。當緩存命中鲤脏,即緩存數(shù)據(jù)庫中有緩存數(shù)據(jù)并且沒有失效们颜,就可以直接返回數(shù)據(jù),不用向服務器發(fā)起請求猎醇。如果沒有命中窥突,即緩存數(shù)據(jù)庫中沒有緩存數(shù)據(jù)或者數(shù)據(jù)失效,那么就要向服務器發(fā)起請求硫嘶,服務器成功返回后阻问,將數(shù)據(jù)保存到數(shù)據(jù)庫。那么上面提到的怎么確定緩存數(shù)據(jù)是否失效呢沦疾?
有兩種方式称近,分別是用ExpiresCache-Control字段
Expires
這個比較簡單贡蓖,就是當向服務器請求資源時,服務器會在響應報文頭部增加Expires字段煌茬,表示這個資源的到期時間,如果下次請求數(shù)據(jù)的時間在這個時間內就直接使用緩存數(shù)據(jù)彻桃,否則就要重新向服務器請求資源坛善。不過這個字段是HTTP1.0的,現(xiàn)在瀏覽器默認使用HTTP1.1邻眷。
Cache-Control
由于Expires過期時間是服務器給的眠屎,可能會和客戶端的時間不一致,從而導致誤差的出現(xiàn)肆饶。所以引入了Cache-Control規(guī)則改衩。Cache-Control定義很多字段:

字段 含義
private 客戶端可以緩存
public 客戶端和代理服務器都可緩存
max-age = xxx 緩存在xxx秒后失效
no-cache 需要使用對比緩存來驗證數(shù)據(jù)
no-store 所有數(shù)據(jù)都不緩存

對比緩存

對比緩存

上面左右分別是緩存命中和不命中的情況,可以看到所謂對比緩存就是當向服務器請求資源時驯镊,服務器會同時給你一個數(shù)據(jù)標識葫督,下次再請求的時候要帶上這個標識,然后服務器會驗證這個標識板惑,如果驗證到這個標識對應的數(shù)據(jù)未失效則返回304告知使用本地緩存數(shù)據(jù)橄镜,否則返回最新的資源以及新的數(shù)據(jù)標識。這個標識有點類似于APP的登錄token冯乘,第一次登錄時服務器會返回一個token洽胶,后續(xù)再登錄只用發(fā)送這個token給服務器就可以。當然這里不叫token裆馒。有下面兩種方式:Last-Modified/If-Modified-SinceEtag/If-None-Match姊氓。下面分別來看下這兩種方式:
Last-Modified/If-Modified-Since
當客戶端向服務器發(fā)起請求時,服務器返回響應報文的同時還會在報文頭部添加該資源最近一次修改的時間喷好,用Last-Modified來表示翔横,后面跟具體時間,這樣當客戶端再次需要這個數(shù)據(jù)時绒窑,要在請求報文頭部增加If-Modified-Since字段棕孙,內容就是之前Last-Modified后面的時間,服務器收到If-Modified-Since的值后些膨,會進行校驗看最近更改時間是否一致蟀俊,如果一致則返回304狀態(tài)碼,告知客戶端資源未更改可直接使用本地緩存订雾,否則會返回新的資源和最近的更改時間肢预。
Etag/If-None-Match
這個流程類似,當客戶端向服務器發(fā)起請求時洼哎,服務器返回響應報文的同時會返回該資源的唯一標識Etag烫映,有點類似token沼本,生成規(guī)則由服務器決定。當客戶端再次發(fā)起請求是需要在報文頭部用If-None-Match字段后面就是上次保存的Etag锭沟,服務器接收到會校驗這個值抽兆,如果資源更新了則這個值就會校驗出錯,那么就會直接返回新的數(shù)據(jù)和新的Etag族淮,否則返回304告知客戶端使用本地緩存辫红。

緩存流程

上面我們看到,HTTP緩存有好幾種方式祝辣,每種方式所用字段也不一樣贴妻,那到底該使用哪種,或者說當同時出現(xiàn)上面的情況蝙斜,以哪個為先名惩,其實這也是由一定流程和優(yōu)先級的。他們的優(yōu)先級和流程圖如下:


流程圖

知道HTTP的緩存機制孕荠,再來看這個CacheInterceptor會容易很多娩鹉,我們來看下,

    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;

首先有個內部cache容器稚伍,如果cache不為空則獲取當前request對應的response底循,否則返回空值。

    long now = System.currentTimeMillis();
    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    Request networkRequest = strategy.networkRequest;
    Response cacheResponse = strategy.cacheResponse;

上面代碼由request和cacheCandidate定義了一個CacheStrategy類槐瑞,CacheStrategy里具體實現(xiàn)其實就是我們上面講的HTTP緩存機制熙涤,然后獲取strategy的networkRequest和cacheResponse,這兩個不一定都有值困檩,有可能為空祠挫,接下來的代碼就是根據(jù)這兩個是否為空的情況來判斷是要網(wǎng)絡請求還是直接使用緩存數(shù)據(jù)庫。

    if (cacheCandidate != null && cacheResponse == null) {
      closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
    }

這句比較好理解悼沿,如果cacheCandidate不為空并且cacheResponse為空等舔,就清空之前的緩存。

    // If we're forbidden from using the network and the cache is insufficient, fail.
    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)絡并且之前也沒緩存慌植,就返回504錯誤。

    // If we don't need the network, we're done.
    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }

如果沒有網(wǎng)路义郑,但之前有緩存蝶柿,則直接返回之前的緩存

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

接下里,如果networkRequest不為空非驮,則進行網(wǎng)絡請求交汤。

    // If we have a cache response too, then we're doing a conditional get.
    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();

        // 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 {
        closeQuietly(cacheResponse.body());
      }
    }

如果之前有緩存,并且上面的網(wǎng)絡請求返回304劫笙,則使用之前的緩存芙扎,并更新cache緩存集合星岗。

    Response response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();
    if (cache != null) {
      if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
        // Offer this request to the cache.
        CacheRequest cacheRequest = cache.put(response);
        return cacheWritingResponse(cacheRequest, response);
      }
      if (HttpMethod.invalidatesCache(networkRequest.method())) {
        try {
          cache.remove(networkRequest);
        } catch (IOException ignored) {
          // The cache cannot be written.
        }
      }
    }
    return response;

如果不是304則說明是新的資源,則接下里就是緩存這個新的response并返回戒洼。
這樣CacheInterceptor這個攔截器就說完了俏橘。

4.ConnectInterceptor

接下來看下連接攔截器ConnectInterceptor,先看下它的intercept方法:

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

    // We need the network to satisfy this request. Possibly for validating a conditional GET.
    boolean doExtensiveHealthChecks = !request.method().equals("GET");
    Exchange exchange = transmitter.newExchange(chain, doExtensiveHealthChecks);

    return realChain.proceed(request, transmitter, exchange);
  }

上面我們可以看到圈浇,這里的核心代碼是通過transmitter的newExchange方法創(chuàng)建一個Exchange對象敷矫,然后把它傳入到下一個攔截器中,這個Exchange可以理解成每次客戶端向服務器請求時進行的數(shù)據(jù)交換汉额,說白了就是后面的攔截器就是通過這個類來進行數(shù)據(jù)的讀寫操作,而這個攔截器做得工作就是與服務器建立連接榨汤,然后提供這個Exchange對象蠕搜。所以接下來重點來看下這個對象是如何被創(chuàng)建出來的。我們進入newExchange方法:

  /** Returns a new exchange to carry a new request and response. */
  Exchange newExchange(Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
    synchronized (connectionPool) {
      if (noMoreExchanges) throw new IllegalStateException("released");
      if (exchange != null) throw new IllegalStateException("exchange != null");
    }
    ExchangeCodec codec = exchangeFinder.find(client, chain, doExtensiveHealthChecks);
    Exchange result = new Exchange(this, call, eventListener, exchangeFinder, codec);
    synchronized (connectionPool) {
      this.exchange = result;
      this.exchangeRequestDone = false;
      this.exchangeResponseDone = false;
      return result;
    }
  }

這里關鍵是這兩行代碼:

    ExchangeCodec codec = exchangeFinder.find(client, chain, doExtensiveHealthChecks);
    Exchange result = new Exchange(this, call, eventListener, exchangeFinder, codec);

先是通過exchangeFinder的find方法獲取一個ExchangeCodec對象收壕,然后利用這個ExchangeCodec對象再創(chuàng)建Exchange對象妓灌。這里可能有人會感到奇怪,這里的exchangeFinder是哪來的蜜宪,其實就在RetryAndFollowUpInterceptortransmitter.prepareToConnect(request);這行代碼里就已經(jīng)初始化好了,可以進入這個方法看下:

  public void prepareToConnect(Request request) {
    if (this.request != null) {
      if (sameConnection(this.request.url(), request.url())) return; // Already ready.
      if (exchange != null) throw new IllegalStateException();

      if (exchangeFinder != null) {
        maybeReleaseConnection(null, true);
        exchangeFinder = null;
      }
    }

    this.request = request;
    this.exchangeFinder = new ExchangeFinder(this, connectionPool, createAddress(request.url()),
        call, eventListener);
  }

然后這里通過exchangeFinder找到一個ExchangeCodec虫埂,這個其實就是一個編碼解碼器,通俗點就是針對不同的協(xié)議比如HTTP1和HTTP2采用讀寫協(xié)議的不同圃验。接下來就繼續(xù)看下這個find方法是如何實現(xiàn)的:

  public ExchangeCodec find(
      OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
    int connectTimeout = chain.connectTimeoutMillis();
    int readTimeout = chain.readTimeoutMillis();
    int writeTimeout = chain.writeTimeoutMillis();
    int pingIntervalMillis = client.pingIntervalMillis();
    boolean connectionRetryEnabled = client.retryOnConnectionFailure();
    try {
      RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
          writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
      return resultConnection.newCodec(client, chain);
    } catch (RouteException e) {
      trackFailure();
      throw e;
    } catch (IOException e) {
      trackFailure();
      throw new RouteException(e);
    }
  }

可以看到里面主要是調用findHealthyConnection這個方法獲取一個客戶端和服務器的連接掉伏,然后調用這個newCodec方法來創(chuàng)建ExchangeCodec把夸,所以接下來就看下findHealthyConnection方法:

  private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
      int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled,
      boolean doExtensiveHealthChecks) throws IOException {
    while (true) {
      RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
          pingIntervalMillis, connectionRetryEnabled);
      // If this is a brand new connection, we can skip the extensive health checks.
      synchronized (connectionPool) {
        if (candidate.successCount == 0) {
          return candidate;
        }
      }
      // Do a (potentially slow) check to confirm that the pooled connection is still good. If it
      // isn't, take it out of the pool and start again.
      if (!candidate.isHealthy(doExtensiveHealthChecks)) {
        candidate.noNewExchanges();
        continue;
      }
      return candidate;
    }
  }

這里代碼也不是很復雜浪册,有一個while循環(huán)挨决,通過findConnection來找到一個連接鞋吉,如果這個連接是一個新的連接就直接返回筷屡,否則還需要做下額外的檢查廊镜,如果這個連接不是健康的連接鞠鲜,就標志這個連接為不可用并且再重新查找連接户辫,這樣不斷循環(huán)直到找到可用的連接麻裁。這里繼續(xù)往下看下findConnection是如何實現(xiàn)的:

  private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
      int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
    boolean foundPooledConnection = false;
    RealConnection result = null;
    Route selectedRoute = null;
    RealConnection releasedConnection;
    Socket toClose;
    synchronized (connectionPool) {
      if (transmitter.isCanceled()) throw new IOException("Canceled");
      hasStreamFailure = false; // This is a fresh attempt.

      Route previousRoute = retryCurrentRoute()
          ? transmitter.connection.route()
          : null;

      // Attempt to use an already-allocated connection. We need to be careful here because our
      // already-allocated connection may have been restricted from creating new exchanges.
      releasedConnection = transmitter.connection;
      toClose = transmitter.connection != null && transmitter.connection.noNewExchanges
          ? transmitter.releaseConnectionNoEvents()
          : null;

      if (transmitter.connection != null) {
        // We had an already-allocated connection and it's good.
        result = transmitter.connection;
        releasedConnection = null;
      }

      if (result == null) {
        // Attempt to get a connection from the pool.
        if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, null, false)) {
          foundPooledConnection = true;
          result = transmitter.connection;
        } else {
          selectedRoute = previousRoute;
        }
      }
    }
    closeQuietly(toClose);

    if (releasedConnection != null) {
      eventListener.connectionReleased(call, releasedConnection);
    }
    if (foundPooledConnection) {
      eventListener.connectionAcquired(call, result);
    }
    if (result != null) {
      // If we found an already-allocated or pooled connection, we're done.
      return result;
    }

    // If we need a route selection, make one. This is a blocking operation.
    boolean newRouteSelection = false;
    if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
      newRouteSelection = true;
      routeSelection = routeSelector.next();
    }

    List<Route> routes = null;
    synchronized (connectionPool) {
      if (transmitter.isCanceled()) throw new IOException("Canceled");

      if (newRouteSelection) {
        // Now that we have a set of IP addresses, make another attempt at getting a connection from
        // the pool. This could match due to connection coalescing.
        routes = routeSelection.getAll();
        if (connectionPool.transmitterAcquirePooledConnection(
            address, transmitter, routes, false)) {
          foundPooledConnection = true;
          result = transmitter.connection;
        }
      }

      if (!foundPooledConnection) {
        if (selectedRoute == null) {
          selectedRoute = routeSelection.next();
        }

        // Create a connection and assign it to this allocation immediately. This makes it possible
        // for an asynchronous cancel() to interrupt the handshake we're about to do.
        result = new RealConnection(connectionPool, selectedRoute);
        connectingConnection = result;
      }
    }

    // If we found a pooled connection on the 2nd time around, we're done.
    if (foundPooledConnection) {
      eventListener.connectionAcquired(call, result);
      return result;
    }

    // Do TCP + TLS handshakes. This is a blocking operation.
    result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
        connectionRetryEnabled, call, eventListener);
    connectionPool.routeDatabase.connected(result.route());

    Socket socket = null;
    synchronized (connectionPool) {
      connectingConnection = null;
      // Last attempt at connection coalescing, which only occurs if we attempted multiple
      // concurrent connections to the same host.
      if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, true)) {
        // We lost the race! Close the connection we created and return the pooled connection.
        result.noNewExchanges = true;
        socket = result.socket();
        result = transmitter.connection;
      } else {
        connectionPool.put(result);
        transmitter.acquireConnectionNoEvents(result);
      }
    }
    closeQuietly(socket);

    eventListener.connectionAcquired(call, result);
    return result;
  }

這個方法里的代碼賊ji兒長箍镜,我們不要慌慢慢來分析下,這里我們先不看那些細枝末節(jié)煎源,挑重點的來看色迂,首先我們來看下這段代碼:

      toClose = transmitter.connection != null && transmitter.connection.noNewExchanges
          ? transmitter.releaseConnectionNoEvents()
          : null;

      if (transmitter.connection != null) {
        // We had an already-allocated connection and it's good.
        result = transmitter.connection;
        releasedConnection = null;
      }

這里首先判斷下當前transmitter內存中的連接是否可用,如果不可用就回收掉手销,如果可用的話直接賦值給result脚草,然后后面就直接返回這個連接。當連接不可用的時候原献,就接著往下執(zhí)行馏慨,主要代碼如下:

      if (result == null) {
        // Attempt to get a connection from the pool.
        if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, null, false)) {
          foundPooledConnection = true;
          result = transmitter.connection;
        } else {
          selectedRoute = previousRoute;
        }
      }
    }

這里通過一個transmitterAcquirePooledConnection方法來獲取一個連接埂淮,這個方法傳入了一個transmitter參數(shù),如果找到可用連接那么transmitter中的connection就是有值的写隶,所以就將transmitter.connection賦值給result倔撞,接下來就看下這個方法:

  boolean transmitterAcquirePooledConnection(Address address, Transmitter transmitter,
      @Nullable List<Route> routes, boolean requireMultiplexed) {
    assert (Thread.holdsLock(this));
    for (RealConnection connection : connections) {
      if (requireMultiplexed && !connection.isMultiplexed()) continue;
      if (!connection.isEligible(address, routes)) continue;
      transmitter.acquireConnectionNoEvents(connection);
      return true;
    }
    return false;
  }

這個方法參數(shù)傳入了Address連接地址,Transmitter傳輸協(xié)議層慕趴,Route路由列表痪蝇,這個Route其實就比Address多了一個代理類型,最后一個參數(shù)是否要求多路復用冕房,然后我們看方法里面具體代碼躏啰,里面是一個對連接池的遍歷,如果當前的連接不是多路復用耙册,但如果requireMultiplexed是true即要求多路復用那就執(zhí)行continue遍歷下一個connection给僵,這里我們傳入的requireMultiplexed值為false,所以會接著執(zhí)行下面的代碼详拙,也就是通過調用connection 的isEligible方法來判斷當前的連接是否可用帝际,如果不可用就接著遍歷下個connection,否則就執(zhí)行下面的代碼獲取這個連接饶辙。我們看下這個isEligible方法:

  boolean isEligible(Address address, @Nullable List<Route> routes) {
    // 如果一個連接已經(jīng)有一個或多個請求或者該連接不可用就直接返回false
    if (transmitters.size() >= allocationLimit || noNewExchanges) return false;

    // 除了主機名外蹲诀,如果當前連接的路由地址和要請求的地址不同就直接返回false
    if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;
    // 如果主機名也一樣說明是同一個連接返回true,表示該連接可用
    if (address.url().host().equals(this.route().address().url().host())) {
      return true; // This connection is a perfect match.
    }
    // 如果域名不一樣弃揽,說明連接不可重用脯爪,但是有一種情況除外,就是如果當前為HTTP2協(xié)議矿微,域名不一樣也是可以重用連接的披粟,這個叫做連接合并,具體連接合并的概念可以參考一下文章
    // https://hpbn.co/optimizing-application-delivery/#eliminate-domain-sharding
    // https://daniel.haxx.se/blog/2016/08/18/http2-connection-coalescing/

    // 1. 當前連接必須為HTTP2冷冗,否則為不可用
    if (http2Connection == null) return false;

    // 2. 不同的路由必須對應到同一臺主機守屉,否則為不可用
    if (routes == null || !routeMatchesAny(routes)) return false;

    // 3. 下面是驗證證書相關的東西
    if (address.hostnameVerifier() != OkHostnameVerifier.INSTANCE) return false;
    if (!supportsUrl(address.url())) return false;

    
    try {
      address.certificatePinner().check(address.url().host(), handshake().peerCertificates());
    } catch (SSLPeerUnverifiedException e) {
      return false;
    }

    return true; // 該連接可重用
  }

上面方法中的代碼都做了注釋,相信還是很好理解的蒿辙。上面我們調用transmitterAcquirePooledConnection方法是傳入的routes為null拇泛,表示只是在連接池中查找HTTP1非多路復用的連接。如果找不到思灌,我們接著再看下面:

    boolean newRouteSelection = false;
    if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
      newRouteSelection = true;
      routeSelection = routeSelector.next();
    }

    List<Route> routes = null;
    synchronized (connectionPool) {
      if (transmitter.isCanceled()) throw new IOException("Canceled");

      if (newRouteSelection) {
        routes = routeSelection.getAll();
        if (connectionPool.transmitterAcquirePooledConnection(
            address, transmitter, routes, false)) {
          foundPooledConnection = true;
          result = transmitter.connection;
        }
      }

      if (!foundPooledConnection) {
        if (selectedRoute == null) {
          selectedRoute = routeSelection.next();
        }

        //創(chuàng)建一個連接(實際連接動作在后面)
        result = new RealConnection(connectionPool, selectedRoute);
        connectingConnection = result;
      }
    }

    // 如果找到則直接返回
    if (foundPooledConnection) {
      eventListener.connectionAcquired(call, result);
      return result;
    }

    // 建立連接
    result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
        connectionRetryEnabled, call, eventListener);
    connectionPool.routeDatabase.connected(result.route());

當上面HTTP1的連接找不到時俺叭,我們當前請求可能有很多其他路由,比如有很多代理服務器泰偿,它們組成一個個IP列表熄守,相當于有很多route,然后把這個route集合傳入transmitterAcquirePooledConnection方法,來查找可多路復用的HTTP2連接裕照。如果還沒找到可用的連接就自己創(chuàng)建一個RealConnection然后調用connect方法建立連接攒发。
建立完連接后我們接著看下:

Socket socket = null;
    synchronized (connectionPool) {
      connectingConnection = null;
      // Last attempt at connection coalescing, which only occurs if we attempted multiple
      // concurrent connections to the same host.
      if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, true)) {
        // We lost the race! Close the connection we created and return the pooled connection.
        result.noNewExchanges = true;
        socket = result.socket();
        result = transmitter.connection;
      } else {
        connectionPool.put(result);
        transmitter.acquireConnectionNoEvents(result);
      }
    }
    closeQuietly(socket);

    eventListener.connectionAcquired(call, result);
    return result;

可以看到連接建立成功后,并不是馬上返回晋南,而是又調用了一次transmitterAcquirePooledConnection方法惠猿,并傳入了routes且requireMultiplexed參數(shù)為true,說明此時是在連接池中只查找多路復用的负间,為啥還要查找一遍偶妖?不是連接已經(jīng)創(chuàng)建成功了?因為假如當我們正好同時進行兩個請求時政溃,可能會出現(xiàn)創(chuàng)建了兩次連接趾访,但是如果這兩個連接符合多路復用,那么就會造成資源浪費董虱,所以每次建立連接后會再檢查遍扼鞋,確認連接池沒有可用連接才返回當前連接。這樣整個連接查找的過程的就分析完了空扎。
接下來我們來簡單看下result.connect方法是如何建立連接的:

if (route.requiresTunnel()) {
  connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener);
  if (rawSocket == null) {
    break;
  }
} else {
  connectSocket(connectTimeout, readTimeout, call, eventListener);
}
establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener);

主要看下上面的關鍵代碼,可以看到润讥,如果需要建立隧道tunnel則先建立tunnel转锈,沒有就直接創(chuàng)建socket連接即TCP連接,建立連接后通過establishProtocol方法來進行協(xié)議握手楚殿,比如HTTPS相關的SSL握手及HTTP2相關協(xié)議撮慨,這里就不展開講了。
上面我們用大量的篇幅講解了連接的獲取和建立脆粥,知道這個流程其實對ConnectInterceptor這個攔截器就已經(jīng)了解得差不多了砌溺,其他的一些細枝末節(jié)稍微再看下就好了。接下來來看下下一個攔截器:

5.CallServerInterceptor

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

    long sentRequestMillis = System.currentTimeMillis();

    exchange.writeRequestHeaders(request);

    boolean responseHeadersStarted = false;
    Response.Builder responseBuilder = null;
    if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
      // If there's a "Expect: 100-continue" header on the request, wait for a "HTTP/1.1 100
      // Continue" response before transmitting the request body. If we don't get that, return
      // what we did get (such as a 4xx response) without ever transmitting the request body.
      if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
        exchange.flushRequest();
        responseHeadersStarted = true;
        exchange.responseHeadersStart();
        responseBuilder = exchange.readResponseHeaders(true);
      }

      if (responseBuilder == null) {
        if (request.body().isDuplex()) {
          // Prepare a duplex body so that the application can send a request body later.
          exchange.flushRequest();
          BufferedSink bufferedRequestBody = Okio.buffer(
              exchange.createRequestBody(request, true));
          request.body().writeTo(bufferedRequestBody);
        } else {
          // Write the request body if the "Expect: 100-continue" expectation was met.
          BufferedSink bufferedRequestBody = Okio.buffer(
              exchange.createRequestBody(request, false));
          request.body().writeTo(bufferedRequestBody);
          bufferedRequestBody.close();
        }
      } else {
        exchange.noRequestBody();
        if (!exchange.connection().isMultiplexed()) {
          // If the "Expect: 100-continue" expectation wasn't met, prevent the HTTP/1 connection
          // from being reused. Otherwise we're still obligated to transmit the request body to
          // leave the connection in a consistent state.
          exchange.noNewExchangesOnConnection();
        }
      }
    } else {
      exchange.noRequestBody();
    }

    if (request.body() == null || !request.body().isDuplex()) {
      exchange.finishRequest();
    }

    if (!responseHeadersStarted) {
      exchange.responseHeadersStart();
    }

    if (responseBuilder == null) {
      responseBuilder = exchange.readResponseHeaders(false);
    }

    Response response = responseBuilder
        .request(request)
        .handshake(exchange.connection().handshake())
        .sentRequestAtMillis(sentRequestMillis)
        .receivedResponseAtMillis(System.currentTimeMillis())
        .build();

    int code = response.code();
    if (code == 100) {
      // server sent a 100-continue even though we did not request one.
      // try again to read the actual response
      response = exchange.readResponseHeaders(false)
          .request(request)
          .handshake(exchange.connection().handshake())
          .sentRequestAtMillis(sentRequestMillis)
          .receivedResponseAtMillis(System.currentTimeMillis())
          .build();

      code = response.code();
    }

    exchange.responseHeadersEnd(response);

    if (forWebSocket && code == 101) {
      // Connection is upgrading, but we need to ensure interceptors see a non-null response body.
      response = response.newBuilder()
          .body(Util.EMPTY_RESPONSE)
          .build();
    } else {
      response = response.newBuilder()
          .body(exchange.openResponseBody(response))
          .build();
    }

    if ("close".equalsIgnoreCase(response.request().header("Connection"))
        || "close".equalsIgnoreCase(response.header("Connection"))) {
      exchange.noNewExchangesOnConnection();
    }

    if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
      throw new ProtocolException(
          "HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
    }

    return response;
  }

這是實際和服務端進行數(shù)據(jù)交互的攔截器变隔,可以看到正如上面所說规伐,它的數(shù)據(jù)交互就是用我們在ConnectInterceptor中創(chuàng)建的Exchange來進行數(shù)據(jù)的讀寫。如果你繼續(xù)深挖下去的話其實可以看到這里數(shù)據(jù)的讀寫操作是用到了Square他們自己家的另一個開源庫okio匣缘,這個庫是專門處理I/O流的讀寫猖闪,比Java自帶那一套API要方便很多,有興趣的同學可以研究下這個庫肌厨,這里就不繼續(xù)展開了培慌。

6.結尾

到這里OkHttp中的攔截器也就都分析完了,攔截器的處理流程也是OkHttp的最妙的地方柑爸,理解了其中攔截器的實現(xiàn)也算是對該庫有了一個很好的理解吵护。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子馅而,更是在濱河造成了極大的恐慌祥诽,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件用爪,死亡現(xiàn)場離奇詭異原押,居然都是意外死亡,警方通過查閱死者的電腦和手機偎血,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門诸衔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人颇玷,你說我怎么就攤上這事笨农。” “怎么了帖渠?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵谒亦,是天一觀的道長。 經(jīng)常有香客問我空郊,道長份招,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任狞甚,我火速辦了婚禮锁摔,結果婚禮上,老公的妹妹穿的比我還像新娘哼审。我一直安慰自己谐腰,他們只是感情好,可當我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布涩盾。 她就那樣靜靜地躺著十气,像睡著了一般。 火紅的嫁衣襯著肌膚如雪春霍。 梳的紋絲不亂的頭發(fā)上砸西,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天,我揣著相機與錄音址儒,去河邊找鬼籍胯。 笑死,一個胖子當著我的面吹牛离福,可吹牛的內容都是我干的杖狼。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼妖爷,長吁一口氣:“原來是場噩夢啊……” “哼蝶涩!你這毒婦竟也來了理朋?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤绿聘,失蹤者是張志新(化名)和其女友劉穎嗽上,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體熄攘,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡兽愤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了挪圾。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片浅萧。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖哲思,靈堂內的尸體忽然破棺而出洼畅,到底是詐尸還是另有隱情,我是刑警寧澤棚赔,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布帝簇,位于F島的核電站,受9級特大地震影響靠益,放射性物質發(fā)生泄漏丧肴。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一胧后、第九天 我趴在偏房一處隱蔽的房頂上張望芋浮。 院中可真熱鬧,春花似錦绩卤、人聲如沸途样。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至陶夜,卻和暖如春凛驮,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背条辟。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工黔夭, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人羽嫡。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓本姥,卻偏偏與公主長得像,于是被迫代替她去往敵國和親杭棵。 傳聞我的和親對象是個殘疾皇子婚惫,可洞房花燭夜當晚...
    茶點故事閱讀 42,877評論 2 345

推薦閱讀更多精彩內容