Android每週一輪子:OkHttp

Jensen95發表於2018-03-19

前言

Okhttp

前兩篇的文章講解了Volley,HttpURLConnection,今天是對於OKHttp的分析,分析完成將會分析OKIO和retrofit,試圖通過這一系列分析,來對Android的網路庫的實現有足夠充分的瞭解,當然這裡的分析完全是脫離了對於專案的具體針對性實踐,因此在一些細節上會有說欠缺,這也將會是接下來原始碼分析的下一步,從專案中的應用和實踐優化作為出發點。

基礎使用

  • 建立OKHttpClient和構造請求
OkHttpClient client = new OkHttpClient.Builder()
        .readTimeout(30, TimeUnit.SECONDS)
        .build();

Request request = new Request.Builder()
        .header("User-Agent", "OkHttp Headers.java")
        .url("http://www.baidu.com")
        .build();
複製程式碼
  • 發起非同步請求
client.newCall(request).enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {
    }

    @Override
    public void onResponse(Call call, okhttp3.Response response) throws IOException {
    }
});
複製程式碼
  • 發起同步請求
okhttp3.Response response = client.newCall(request).execute();
複製程式碼

OkHttp的使用首先通過OkHttp的生成器來根據我們自己的配置建立一個OKHttpClient,然後構造一個請求,設定請求的header,url,請求的body,OkHttp提供了同步和非同步兩種請求執行方式。這裡可以根據自己的需求選擇一個合適的方式。

實現分析

按照Android每週一輪子的寫作風格,基礎使用作為一個引子,幫助我們迅速的切入框架的執行流程,快速的理清整個呼叫鏈路,瞭解該框架的實現,此處我們還是延續這種方式。針對上面的使用流程,逐步分析。

  • newCall
public Call newCall(Request request) {
  return RealCall.newRealCall(this, request, false /* for web socket */);
}
複製程式碼
static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
  RealCall call = new RealCall(client, originalRequest, forWebSocket);
  call.eventListener = client.eventListenerFactory().create(call);
  return call;
}
複製程式碼

根據建立的請求,構造一個RealCall例項,同時為該RealCall物件設定事件監聽器。在EventListener中,定義了許多的函式,可以監控到網路請求的整個生命週期,包括DNS開始查詢,DNS查詢結束,連線的開始建立,連線建立失敗等等。

  • enqueue

在連線建立之後,在非同步請求中,將會呼叫enqueue方法。

public void enqueue(Callback responseCallback) {
  synchronized (this) {
    if (executed) throw new IllegalStateException("Already Executed");
    executed = true;
  }
  captureCallStackTrace();
  eventListener.callStart(this);
  client.dispatcher().enqueue(new AsyncCall(responseCallback));
}
複製程式碼

在該方法中,會呼叫OkHttpClient的Dispatcher的enqueue方法。在建立OkHttpClient的時候,但開發者未設定Dispatcher時,會預設建立一個Dispatcher。這裡按照預設的實現程式碼進行分析。

synchronized void enqueue(AsyncCall call) {
  if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
    runningAsyncCalls.add(call);
    executorService().execute(call);
  } else {
    readyAsyncCalls.add(call);
  }
}
複製程式碼

加入到Dispatchr之中之後,對於請求進行判斷,判斷是否超過了最大請求數目,是否超過了單個host共享下的請求數目,如果超過了則將其加入到準備執行佇列之中。如果請求數目過多的時候,將其放置在一個準備佇列之中。對於ArrayDeque的資料結構解釋文末。網路請求執行,是通過執行緒池來實現的,對於執行緒池的建立和具體的執行問題,將在文末具體分析。

在Dispatcher中用來管理請求的資料結構,分別為正在執行的非同步請求佇列,正在準備的非同步請求佇列,正在執行的同步請求佇列。

Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
複製程式碼
  • execute

對於請求的具體執行過程,在AsyncCall的execute方法中。

final class AsyncCall extends NamedRunnable {

  @Override protected void execute() {
    boolean signalledCallback = false;
    try {
      Response response = getResponseWithInterceptorChain();
         .....
    } catch (IOException e) {
      ......
    } finally {
      client.dispatcher().finished(this);
    }
  }
}
複製程式碼

這裡首先獲得響應結果,在獲得響應結果的時候,可能會出現一些異常情況,這裡會catch到異常,會回撥事件監聽器的一些回撥函式。對於獲取請求響應結果的核心呼叫就是getResponseWithInterceptorChain通過層層責任鏈的執行來獲得最終的請求結果。

  • getResponseWithInterceptorChain
Response getResponseWithInterceptorChain() throws IOException {
  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);
}
複製程式碼

構建一個Interceptor列表,在其中新增使用者設定的攔截器,框架自身的快取,網路連線等等聯結器,然後根據這一系列的攔截器構建出一個Interceptor.Chain例項物件,之後呼叫其processed方法。

  • 責任鏈的執行(proceed)

proceed的方法是網路請求執行的核心

public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
    RealConnection connection) throws IOException {
  if (index >= interceptors.size()) throw new AssertionError();

  calls++;

  //如果已經有存在的流,確定進入的請求可以使用它
  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 (this.httpCodec != null && calls > 1) {
    throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
        + " must call proceed() exactly once");
  }

  // 呼叫該鏈中的下一個攔截器
  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);

//確認攔截器是否呼叫了chain的proceed方法。
  if (httpCodec != null && index + 1 < interceptors.size() && next.calls != 1) {
    throw new IllegalStateException("network interceptor " + interceptor
        + " must call proceed() exactly once");
  }

  // 判斷響應是否為空,為空丟擲異常
  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;
}
複製程式碼

責任鏈的處理函式執行的操作大致為對於一些狀態進行判斷,然後取其中的攔截器構造一個攔截鏈例項,然後執行攔截器的攔截方法,在其攔截方法中還會繼續呼叫建立的新的RealChain方法的proceed方法,通過這種遞迴的方式來將資料進行層層包裝處理,最終將資料丟回。對於其中的每一個攔截器在完成整個網路請求的過程發揮了關鍵的作用。接下來從第一個攔截器開始逐層次進行分析。

RetryAndFollowUpInterceptor

這個攔截器可以將其從失敗和有必要的重定向中恢復。多少重定向和授權需要嘗試,Chrome會跟隨21次重定向,火狐,curl,wget會跟隨20次,Safari會跟隨16次,Http1.0推薦5次,這裡則為20次。

通過一個While true死迴圈來進行重試操作,在這裡建立StreamAllocation,然後建立新的攔截器鏈例項,然後呼叫其processed方法,等待返回結果回來。該攔截器在最外層,接下來的攔截器返回的響應結果,最終都會返回到這裡被處理。

  @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) {
           .....
      Response response;
      boolean releaseConnection = true;
      try {
        response = realChain.proceed(request, streamAllocation, null, null);
        releaseConnection = false;
      } catch (RouteException e) {
     
      //從失敗的路由中恢復
        if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
          throw e.getLastConnectException();
        }
        releaseConnection = false;
        continue;
      } catch (IOException e) {
      
        boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
        if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
        releaseConnection = false;
        continue;
      } finally {
        
        if (releaseConnection) {
          streamAllocation.streamFailed(null);
          streamAllocation.release();
        }
      }
        

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

這裡通過異常捕捉的方式,根據相應的異常出錯,採取相應的恢復方式,同時記錄相應的出錯狀態,但達到閥值之後,停止進行拉起重試操作。此次網路請求失敗。

BridgeInterceptor

應用程式碼和網路程式碼之間的橋樑,用來根據使用者的一個請求構建一個網路請求,最後,根據網路響應來構建一個使用者響應。根據設定的一些頭部引數進行相應的處理。比如GZIP壓縮問題等等。

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

//Gzip 壓縮轉換
  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();
}
複製程式碼

作為應用層和網路層的橋樑,其主要目的是在請求到達網路層時,對網路請求進行包裝和在網路請求的響應結果回來時,對響應結果進行包裝,轉到使用者層。

CacheInterceptor

用來檢測快取中是否有資料,有檢測無變化返回,否則網路請求後存放。

public Response intercept(Chain chain) throws IOException {
  Response cacheCandidate = cache != null
      ? cache.get(chain.request())
      : null;

  long now = System.currentTimeMillis();
  //獲取請求的cache策略
  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()); 
  }

  // 如果我們既無法進行網路請求又無快取,返回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();
  }

  // 如果我們不需要網路請求,執行完成返回結果
  if (networkRequest == null) {
    return cacheResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .build();
  }

//執行責任鏈,根據網路請求獲取響應結果
  Response networkResponse = null;
  try {
    networkResponse = chain.proceed(networkRequest);
  } finally {
    // If we're crashing on I/O or otherwise, don't leak the cache body.
    if (networkResponse == null && cacheCandidate != null) {
      closeQuietly(cacheCandidate.body());
    }
  }

  // 獲取請求響應後,根據cache狀態,對cache內容進行相應的處理
  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 {
      closeQuietly(cacheResponse.body());
    }
  }

//構造一個響應體
  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;
}
複製程式碼

ConnectInterceptor

根據傳遞的資料,尋找並建立一個健康的連線

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

  boolean doExtensiveHealthChecks = !request.method().equals("GET");
  HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);

//獲取一個健康連線
  RealConnection connection = streamAllocation.connection();

  return realChain.proceed(request, streamAllocation, httpCodec, connection);
}
複製程式碼

尋找一個健康的連線,然後將連線傳遞給下一個攔截器進行處理。

CallServerInterceptor

資料的寫入過程,也就是發起網路請求和Server進行互動的過程,然後返回請求資料,這個時候再是層層的返回撥用棧,將資料倒回去,然後進行層層的處理。

@Override public Response intercept(Chain chain) throws IOException {
  RealInterceptorChain realChain = (RealInterceptorChain) chain;
  HttpCodec httpCodec = realChain.httpStream();
  StreamAllocation streamAllocation = realChain.streamAllocation();
  RealConnection connection = (RealConnection) realChain.connection();
  Request request = realChain.request();

  long sentRequestMillis = System.currentTimeMillis();

//請求Event記錄
  realChain.eventListener().requestHeadersStart(realChain.call());
  httpCodec.writeRequestHeaders(request);
  realChain.eventListener().requestHeadersEnd(realChain.call(), request);

  Response.Builder responseBuilder = null;
  if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
    
    if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
      httpCodec.flushRequest();
      realChain.eventListener().responseHeadersStart(realChain.call());
      responseBuilder = httpCodec.readResponseHeaders(true);
    }

    if (responseBuilder == null) {
 
  //開始寫入網路請求
      realChain.eventListener().requestBodyStart(realChain.call());
      long contentLength = request.body().contentLength();
      CountingSink requestBodyOut =
          new CountingSink(httpCodec.createRequestBody(request, contentLength));
      BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
      request.body().writeTo(bufferedRequestBody);
      bufferedRequestBody.close();

      realChain.eventListener()
          .requestBodyEnd(realChain.call(), requestBodyOut.successfulCount);
    } else if (!connection.isMultiplexed()) {
      streamAllocation.noNewStreams();
    }
  }

//完成網路請求
  httpCodec.finishRequest();

//讀取響應內容
  if (responseBuilder == null) {
    realChain.eventListener().responseHeadersStart(realChain.call());
    responseBuilder = httpCodec.readResponseHeaders(false);
  }

//獲取響應體
  Response response = responseBuilder
      .request(request)
      .handshake(streamAllocation.connection().handshake())
      .sentRequestAtMillis(sentRequestMillis)
      .receivedResponseAtMillis(System.currentTimeMillis())
      .build();

//根據Response Code做相應的處理
  int code = response.code();
  if (code == 100) {
    responseBuilder = httpCodec.readResponseHeaders(false);

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

    code = response.code();
  }

  realChain.eventListener()
          .responseHeadersEnd(realChain.call(), response);

//Code值101是協議升級程式碼,這裡首個判斷為向websocket的升級
  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(httpCodec.openResponseBody(response))
        .build();
  }

  return response;
}
複製程式碼

OKHttp實現原理分析

小結

本篇文章算不上對於原始碼的深度剖析,大概還是停留在表層的程式碼邏輯呼叫,對於其中用到的設計模式,各種技術,和其相比於其它網路庫的優勢所在,這裡暫時都沒有做分析,由於本週時間比較近,所以對於效能,優勢特徵,將會在接下來的一篇深度展開分析。同時下一篇也將會作為對OkIO分析的一個引子。

相關文章