1. 程式人生 > >OkHttp原始碼解析(二)

OkHttp原始碼解析(二)

上一篇講到OkHttp的整體流程,但是裡面有一個很重要的方法getResponseWithInterceptorChain還沒有講到,這個方法裡面包括了整個okHttp最重要的攔截器鏈,所以我們今天來講解一下。

Response getResponseWithInterceptorChain() throws IOException {
  // Build a full stack of interceptors.
  List<Interceptor> interceptors = new ArrayList<>();
  interceptors.addAll(client.
interceptors()); interceptors.add(retryAndFollowUpInterceptor); 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, null, null, null, 0, originalRequest, this, eventListener, client.connectTimeoutMillis(), client.readTimeoutMillis(), client.writeTimeoutMillis
()); return chain.proceed(originalRequest); }

這裡建立了一個攔截器list的集合,首先會把使用者自定義的攔截器(應用程式攔截器),和okhttp內部提供的攔截器新增進去,而接下來我們著重講解的是okhttp內部提供的這5個攔截器。然後在第13行這裡,建立了一個RealInterceptorChain的一個例項,在這個例項裡面把我們剛剛新增完的list集合傳遞進去,還有一個index值為0也傳遞進去(這個非常重要),還有request。最後就是呼叫這個chain的proceed方法,而這個攔截器鏈式如何運轉,祕密應該就是在這個方法裡面了,我們就繼續往下看:

@Override public Response proceed(Request request) throws IOException {
  return proceed(request, streamAllocation, httpCodec, connection);
}
public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
    RealConnection connection) throws IOException {
  if (index >= interceptors.size()) throw new AssertionError();
  calls++;
  // If we already have a stream, confirm that the incoming request will use it.
  if (this.httpCodec != null && !this.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.httpCodec != 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, streamAllocation, httpCodec,
      connection, index + 1, request, call, eventListener, 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 (httpCodec != 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;
}

看到它這裡會繼續呼叫它的proceed過載方法,然後看第19行,它這裡又建立了一個新的攔截器,而這個攔截器鏈跟剛才建立的有什麼區別呢?就是傳進去的index值是index+1,這樣子如果我們下次要進行訪問的話,就只能從下一個攔截器進行訪問,這樣子就可以把我們的整個攔截器構成一個鏈條。下一行就是獲取索引index的攔截器,然後再呼叫攔截器的intercept方法,並且將剛剛建立好的next攔截器鏈傳遞進去。為了分析我們整個攔截器鏈的流程,我們對第一個攔截器RetryAndFollowUpInterceptor的intercept方法進行粗略分析。

response = realChain.proceed(request, streamAllocation, null, null);

這裡這個realChain就是之前建立好的next攔截器鏈,這裡會繼續它的proceed方法。呼叫了proceed方法之後,又會繼續建立下一個攔截器鏈,並且獲取當前的攔截器,這樣子就構成了我們整個攔截器鏈的工作流程。

在這裡插入圖片描述

而在攔截器裡面,主要是三個步驟: 1、請求前對request進行處理 2、繼續呼叫下一個攔截器而獲取response 3、對response進行處理,返回給上一個攔截器

上面講完了整個攔截器鏈的工作流程,我們接下來分析各個攔截器的工作流程。

  • RetryAndFollowUpInterceptor

從名字上可以看得出,它是重定向攔截器,失敗重連的。我們來看看它的intercept方法:

@Override public Response intercept(Chain chain) throws IOException {
  Request request = chain.request();
  RealInterceptorChain realChain = (RealInterceptorChain) chain;
  Call call = realChain.call();
  EventListener eventListener = realChain.eventListener();
  StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
      createAddress(request.url()), call, eventListener, callStackTrace);
  this.streamAllocation = streamAllocation;
  int followUpCount = 0;
  Response priorResponse = null;
  while (true) {
    if (canceled) {
      streamAllocation.release();
      throw new IOException("Canceled");
    }
    Response response;
    boolean releaseConnection = true;
    try {
      response = realChain.proceed(request, streamAllocation, null, null);
      releaseConnection = false;
    } catch (RouteException e) {
      // The attempt to connect via a route failed. The request will not have been sent.
      if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
        throw e.getLastConnectException();
      }
      releaseConnection = false;
      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, streamAllocation, requestSendStarted, request)) throw e;
      releaseConnection = false;
      continue;
    } finally {
      // We're throwing an unchecked exception. Release any resources.
      if (releaseConnection) {
        streamAllocation.streamFailed(null);
        streamAllocation.release();
      }
    }
    // 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();
    }
    Request followUp = followUpRequest(response, streamAllocation.route());
    if (followUp == null) {
      if (!forWebSocket) {
        streamAllocation.release();
      }
      return response;
    }
    closeQuietly(response.body());
    if (++followUpCount > MAX_FOLLOW_UPS) {
      streamAllocation.release();
      throw new ProtocolException("Too many follow-up requests: " + followUpCount);
    }
    if (followUp.body() instanceof UnrepeatableRequestBody) {
      streamAllocation.release();
      throw new HttpRetryException("Cannot retry streamed HTTP body", response.code());
    }
    if (!sameConnection(response, followUp.url())) {
      streamAllocation.release();
      streamAllocation = new StreamAllocation(client.connectionPool(),
          createAddress(followUp.url()), call, eventListener, callStackTrace);
      this.streamAllocation = streamAllocation;
    } else if (streamAllocation.codec() != null) {
      throw new IllegalStateException("Closing the body of " + response
          + " didn't close its backing stream. Bad interceptor?");
    }
    request = followUp;
    priorResponse = response;
  }
}

我們看到第6行這裡建立了一個StreamAllocation,這個是用於獲取連線伺服器的connection和資料傳輸的輸入輸出流。雖然這個例項是在這裡建立,但是它並不是在這個攔截器裡面執行,而是一步一步往下傳遞,到後面的攔截器才會被執行。 在第19行這裡就會呼叫realChain.proceed,會繼續呼叫下一個攔截器去獲取response。既然是失敗重連,那這裡又是怎樣實現失敗重連的呢?我們看一下這裡:

Request followUp = followUpRequest(response, streamAllocation.route());
if (followUp == null) {
  if (!forWebSocket) {
    streamAllocation.release();
  }
  return response;
}

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

如果獲取的response是符合條件的話,就會直接把這個response返回。如果不符合條件的話,就會繼續往下走,這裡有一個MAX_FOLLOW_UPS變數,這個變數是在這個攔截器裡面定義的。

private static final int MAX_FOLLOW_UPS = 20;

因為我們重連也不可能無休止地進行重連,所以okhttp這裡就規定了重連的次數最多隻能是20次。

  • BridgeInterceptor

@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();
    if (contentLength != -1) {
      requestBuilder.header("Content-Length", Long.toString(contentLength));
      requestBuilder.removeHeader("Transfer-Encoding");
    } else {
      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");
  }
  // If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing
  // the transfer stream.
  boolean transparentGzip = false;
  if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
    transparentGzip = true;
    requestBuilder.header("Accept-Encoding", "gzip");
  }
  List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
  if (!cookies.isEmpty()) {
    requestBuilder.header("Cookie", cookieHeader(cookies));
  }
  if (userRequest.header("User-Agent") == null) {
    requestBuilder.header("User-Agent", Version.userAgent());
  }
  Response networkResponse = chain.proceed(requestBuilder.build());
  HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());
  Response.Builder responseBuilder = networkResponse.newBuilder()
      .request(userRequest);
  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)));
  }
  return responseBuilder.build();
}

這個攔截器主要的作用就是設定我們的內容長度,編碼方式,壓縮,新增頭部資訊等。 當獲取到response之後,看到最後有一個if的判斷,transparentGzip是在我們的設定“Accept-Encoding”時賦值的,這個標誌位其實就是告訴伺服器我們客戶端是支援gzip壓縮的,那麼這樣子伺服器才會返回一個支援gzip壓縮的response回來。第二個判斷其實就是判斷伺服器返回來的Response是否經過了gzip壓縮,根據“Content-Encoding”進行判斷。第三個條件就是判斷Http頭部是否有body。當三個條件都符合的情況下,我們就會將返回的response的body體的輸入流轉化為GzipSource,這樣子的目的就是我們使用者可以直接以解壓的方式讀取這個資料流。

這樣子就講完了前兩個攔截器,後面會繼續講解剩下的三個攔截器,謝謝!