OkHttp源碼解析

一、引言

在我們日常開發中,OkHttp可謂是最常用的開源庫之一,目前就連Android API中的網絡請求接口都是用的OkHttp,好吧,真的很強。

在上學期間我也曾閱讀和分析過OkHttp的源碼,並記錄在筆記中,不過現在再去翻看的時候發現當時很多地方並沒有正確理解,因此也趁着這個過年假期重新閱讀和整理一遍,感興趣的童鞋可以看看。

本文純屬基於個人理解,因此受限於知識水平,有些地方可能依然沒有理解到位,還請發現問題的童鞋理性指出。

溫馨提示: 本文很長,源碼基於 OKHttp-3.11.0

二、從一個簡單請求入手分析整體運作流程

1. 先來點關於 OkHttpClient 的官方釋義

OkHttpClient作爲Call的工廠類,用於發送HTTP請求並讀取相應數據。

OkHttpClient應當被共享.

使用OkHttpClient的最佳使用方式是創建一個OkHttpClient單例,然後複用這個單例進行所有的HTTP請求。爲啥呢?因爲每個OkHttpClient自身都會持有一個連接池和線程池,所以符用連接和線程可以減少延遲、節約內存。相反地,如果給每個請求都創建一個OkHttpClient的話,那就是浪費閒置線程池的資源。

可以如下使用new OkHttpClient()創建一個默認配置的共享OkHttpClient.

public final OkHttpClient client = new OkHttpClient();

或使用 new OkHttpClient.Builder()來創建一個自定義配置的共享實例:

public final OkHttpClient client = new OkHttpClient.Builder()
    .addInterceptor(new HttpLoggingInterceptor())
    .cache(new Cache(cacheDir, cacheSize))
    .build();

可以通過newBuilder()來自定義OkHttpClient,這樣創建出來的OkHttpClient具有與原對象相同的連接池、線程池和配置。使用這個方法可以派生一個具有自己特殊配置的OkHttpClient以符合我們的特殊要求。

如下示例演示的就是如何派生一個讀超時爲500毫秒的OkHttpClient:

OkHttpClient eagerClient = client.newBuilder()
    .readTimeout(500, TimeUnit.MILLISECONDS)
    .build();
Response response = eagerClient.newCall(request).execute();

關閉(Shutdown)不是必須的

線程和連接會一直被持有,直到當它們保持閒置時自動被釋放。但是如果你編寫的應用需要主動釋放無用資源,那麼你也可以主動去關閉。通過shutdown()方法關閉分發器dispatcher的執行服務,這將導致之後OkHttpClient收到的請求全部被拒絕掉。

client.dispatcher().executorService().shutdown();

清空連接池可以用evictAll(),不過連接池的守護線程可能不會馬上退出。

client.connectionPool().evictAll();

如果相關比緩存,可以調用close(),注意如果緩存已經關閉了再創建call的話就會出現錯誤,並且會導致call崩潰。

client.cache().close();

OkHttp同樣會爲所有HTTP/2連接建立守護線程,並且再它們保持閒置狀態時自動關閉掉它們。

2. 常規使用

上面的官方釋義描述了OkHttpClient的最佳實踐原則和清理操作,接下來我們根據一個簡單的GET請求操作來引出我們要分析的問題:

如下創建一個OkHttpClient實例,添加了Intercepter,並在工程目錄下建了個名爲cacheCache緩存:

Interceptor logInterceptor = chain -> {

    Request request = chain.request();
    System.out.println(request.url());
    System.out.println(request.method());
    System.out.println(request.tag());
    System.out.println(request.headers());

    return chain.proceed(request);
};

okHttpClient = new OkHttpClient.Builder()
        .cache(new Cache(new File("cache/"), 10 * 1024 * 1024))
        .addInterceptor(logInterceptor)
        .build();

然後一個普通的GET請求是這樣的,這裏以獲取 玩Android 首頁列表爲例。

public void getHomeList(int page){
    // 1. 建立HTTP請求
    Request request = new Request.Builder()
            .url(String.format("http://wanandroid.com/article/list/%d/json", page))
            .get()
            .build();
    // 2. 基於 Request創建 Call
    okhttp3.Call call = okHttpClient.newCall(request);
    // 3. 執行Call
    call.enqueue(new Callback() {
        @Override
        public void onFailure(okhttp3.Call call, IOException e) {
            e.printStackTrace();
        }
        @Override
        public void onResponse(okhttp3.Call call, Response response) throws IOException {
            System.out.println(response.message());
            System.out.println(response.code());
            System.out.println(response.headers());

            if (response.isSuccessful()){
                ResponseBody body = response.body();
                if (body == null) return;

                System.out.println(body.string());
                // 每個ResponseBody只能使用一次,使用後需要手動關閉
                body.close();
            }
        }
    });
}

3. 執行流程分析

注意到上面的okHttpClient.newCall(request),對應的源碼如下,可知它創建的實際上是Call的實現類RealCall

@Override 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;
}

Call提供請求任務的執行和取消和相關狀態操作方法。類似於FutureTask,是任務執行單元,其核心的執行方法代碼如下,包含同步執行(execute())和異步執行(enqueue())兩種方式。對於同步方法而言,RealCall僅僅通過executed()方法將自身記錄在Dispatcher(分發器)的同步請求隊列中,這是爲了在分發器中統計請求數量,在請求結束之後則通過finished()方法將自身從分發器中的同步請求隊列中移除,而真正進行數據請求的是在攔截器Intercepter,如下源碼:


@Override public Response execute() throws IOException {
    // ...
    eventListener.callStart(this);
    try {
      // 1. 僅僅將這個 Call記錄在分發器 ( Dispatcher )的同步執行隊列中
      client.dispatcher().executed(this);
      // 2. 通過攔截器鏈獲取響應數據,這裏纔會真正的執行請求
      Response result = getResponseWithInterceptorChain();
      if (result == null) throw new IOException("Canceled");
      return result;
    } catch (IOException e) {
      eventListener.callFailed(this, e);
      throw e;
    } finally {
      // 3.  拿到響應數據後從分發器的同步執行隊列中移除當前請求
      client.dispatcher().finished(this);
    }
}

跟進至getResponseWithInterceptorChain(),可以注意到,除了我們在創建OkHttpClient時添加的攔截器外,每個HTTP請求都會默認添加幾個固有的攔截器,如
RetryAndFollowUpInterceptorBridgeInterceptorCacheInterceptorConnectInterceptorCallServerInterceptor

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

關於它們的源碼實現會在後面的核心類解讀中詳細分析,這裏先了解一個它們各自的作用:

  • RetryAndFollowUpInterceptor:用於失敗時恢復以及在必要時進行重定向。
  • BridgeInterceptor:應用代碼與網絡代碼的橋樑。首先根據用戶請求建立網絡請求,然後執行這個網絡請求,最後根據網絡請求的響應數據建立一個用戶響應數據。
  • CacheInterceptor:用於從本地緩存中讀取數據,以及將服務器數據寫入到緩存。
  • ConnectInterceptor:用於打開一個到目標服務器的連接,並切換至下一個攔截器。
  • CallServerInterceptor:這是攔截器鏈的最後一環,至此將真正的進行服務器請求。

請求時整個攔截器的調用鏈的執行次序如下

對於請求時攔截器的調用鏈你可能會有所疑惑,爲什麼它是按這個次序執行的呢?咱看看RealInterceptorChain#proceed(...)方法的主要源碼,發現,雖然這裏看起來只進行了一次調用,但是如果你結合這些攔截器一起分析的話,你就會發現,其實這裏對攔截器集合進行了遞歸取值,因爲每次執行proceed()方法時集合索引index+1, 並將index傳入新建的RealInterceptorChain,而攔截器集合唯一,因此相當於每次proceed都是依次取得攔截器鏈中的下一個攔截器並使用這個新建的RealInterceptorChain,執行RealInterceptorChain#proceed方法,直到集合遞歸讀取完成。

public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec, RealConnection connection) throws IOException {
    // ...
    // 每次執行 proceed() 方法時 index+1, 然後傳入新建的 RealInterceptorChain
    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);
    // ...
    return response;
}

遞歸? 是的,如果你觀察的夠仔細的話,你會發現,其實BridgeInterceptorRetryAndFollowUpInterceptorCacheInterceptorConnectInterceptor都會執行RealInterceptorChain#proceed方法,相當於這個方法在不斷地調用自己,符合遞歸的執行特性,因此Response響應數據的返回次序剛好是與請求時相反的。BridgeInterceptor#intercept相應抽取的源碼如下:

public final class BridgeInterceptor implements Interceptor {

  @Override public Response intercept(Chain chain) throws IOException {
  // do something ...
    Response networkResponse = chain.proceed(requestBuilder.build());
  // do something ...
    return responseBuilder.build();
  }
}

因而攔截器鏈的響應數據返回次序如下:

我靠,是不是覺得設計的非常巧妙,這也是我熱衷於源碼的重要原因之一,因爲不看看別人的代碼你就永遠不知道別人有多騷。。

根據上面的分析,我們已經知道了原來正真執行請求、處理響應數據是在攔截器,並且對於同步請求,分發器Dispatcher僅僅是記錄下了同步請求的Call,用作請求數量統計用的,並沒有參與到實際請求和執行中來。

OK,來看看異步請求RealCall#enqueue()Dispatcher#enqueue(),毫無疑問,異步請求肯定是運行在線程池中了

@Override 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));
}

Dispatcher#enqueue()

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

對於上面的AsyncCall,核心源碼如下,注意到getResponseWithInterceptorChain(),是不是非常地熟悉了,在上面的同步請求那裏已經詳細解釋過了,就不再累贅了。

  final class AsyncCall extends NamedRunnable {
     // ...
    @Override protected void execute() {
      boolean signalledCallback = false;
      try {
        Response response = getResponseWithInterceptorChain();
        // ...
      } catch (IOException e) {
        // ...
      } finally {
        client.dispatcher().finished(this);
      }
    }
  }

至此,OkHttp的主體運作流程是不是已經清晰了,不過有沒有感覺還少點什麼,我們只是分析了運作流程,具體到怎麼連接的問題還沒有分析。

好吧,既然是建立連接,那麼極速定位到ConnectInterceptor,沒毛病吧, 核心源碼如下:

public final class ConnectInterceptor implements Interceptor {
  public final OkHttpClient client;
  // ...
  @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);
  }
}

注意到上面的streamAllocation.newStream(..),源碼如下:

public HttpCodec newStream( OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
      // ... 
      // 1. 查找可用連接
      RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
          writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
      // 2. 建立 HTTP 或 HTTP2 連接
      HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);

      synchronized (connectionPool) {
        codec = resultCodec;
        return resultCodec;
      }
}

繼續定位到 findHealthyConnection

private RealConnection findHealthyConnection(int connectTimeout, int readTimeout, int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled, boolean doExtensiveHealthChecks) throws IOException {
    while (true) { // 完全阻塞式查找,找不到不罷休
      // 1. 查找已有連接
      RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
          pingIntervalMillis, connectionRetryEnabled);

      // 2. 如果這是一條全新的連接,那麼可以跳過大量的健康檢查,直接返回
      synchronized (connectionPool) {
        if (candidate.successCount == 0) {
          return candidate;
        }
      }
      // 3. 做一個速度超慢的檢查,以確保池中的連接仍然可用,
      //    如果不可用了就將其從池中剔除,然後繼續查找
      if (!candidate.isHealthy(doExtensiveHealthChecks)) {
        noNewStreams();
        continue;
      }
      return candidate;
    }
}

定位到StreamAllocation#findConnection,這裏查找連接的規則是:先查看當前是否存在可用連接,如果不存在,再從連接池中查找,如果還沒有,那就新建一個,用來承載新的數據流。 需要注意的一個細節就是,從連接池查找連接時會查詢兩次,第一次只是根據當前目標服務器地址去查,如果沒有查到,則第二次會重新選擇路由表,然後用該地址去匹配。最終如果存在已經創建好的連接,則直接返回使用,如果不存在,則新建一個連接,進行TCPTLS握手,完事之後將這個連接的路由信息記錄在路由表中,並把這個連接保存到連接池。還需要注意的一點是:如果有個連接與當前建立的連接的地址相同,那麼將釋放掉當前建立好的連接,而使用後面創建的連接(保證連接是最新的)

private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
      int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
    synchronized (connectionPool) {
        // ... 
      // 1. 檢查當前有沒有可用連接,如果有,那麼直接用當前連接
      releasedConnection = this.connection;
      toClose = releaseIfNoNewStreams();
      if (this.connection != null) { // 可用
        result = this.connection;
        releasedConnection = null;
      }
      // ...
      if (result == null) { 
        // 2. 不存在已有連接或者已有連接不可用,則嘗試從連接池中獲得可用連接
        Internal.instance.get(connectionPool, address, this, null);
        if (connection != null) {
          foundPooledConnection = true;
          result = connection;
        } else {
          selectedRoute = route;
        }
      }
    }
    closeQuietly(toClose);
    // ...
    if (result != null) { // 已有連接中找到了連接,完成任務
      return result;
    }

    boolean newRouteSelection = false;
    // 選擇一條路由,這是個阻塞式操作
    if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
      newRouteSelection = true;
      routeSelection = routeSelector.next();
    }

    synchronized (connectionPool) {
      if (canceled) throw new IOException("Canceled");
      if (newRouteSelection) {
          // 路由已經選好了,此時再根據路由中的 IP集合去匹配連接池中的連接,
          // 這個可能因爲連接合並的緣故而匹配到
        List<Route> routes = routeSelection.getAll();
        for (int i = 0, size = routes.size(); i < size; i++) {
          Route route = routes.get(i);
          Internal.instance.get(connectionPool, address, this, route);
          if (connection != null) {
            foundPooledConnection = true;
            result = connection;
            this.route = route;
            break;
          }
        }
      }

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

        // 3. 最後實在沒找到已有的連接,那麼就只能重新建立連接了
        route = selectedRoute;
        refusedStreamCount = 0;
        result = new RealConnection(connectionPool, selectedRoute);
        acquire(result, false);
      }
    }

    // 根據路由匹配到了連接池中的連接
    if (foundPooledConnection) {
      eventListener.connectionAcquired(call, result);
      return result;
    }

    // 進行 TCP + TLS 握手. 這是阻塞式操作
    result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
        connectionRetryEnabled, call, eventListener);
    routeDatabase().connected(result.route()); // 路由表中記錄下這個連接的路由信息

    Socket socket = null;
    synchronized (connectionPool) {
      reportedAcquired = true;
      // 將這個連接記錄到連接池
      Internal.instance.put(connectionPool, result);
      // 如果多個連接指向當前創建的連接的相同地址,那麼釋放掉當前連接,使用後面創建的連接
      if (result.isMultiplexed()) {
        socket = Internal.instance.deduplicate(connectionPool, address, this);
        result = connection;
      }
    }
    closeQuietly(socket);

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

根據以上分析可得出以下主體執行流程:

當然這是同步請求的流程,而對於異步請求而言,也僅僅是把攔截器鏈放到了線程池執行器中執行而已。

三、核心類解讀

至此,我們已經清楚了OkHttp的主幹,當然,我們僅僅是把流程給走通了,在本節中,我們將根據源碼具體分析OkHttp中各核心類的作用及其實現,內容很長,請做好心理準備。

1. 攔截器(Intercepter

1). RetryAndFollowUpInterceptor

作用:用於失敗時恢復以及在必要時進行重定向。

作爲核心方法,RetryAndFollowUpInterceptor#intercept體現了RetryAndFollowUpInterceptor的工作流程,源碼如下,我們來分析分析它是怎麼恢復和重定向的,具體實現流程還請看註釋:

  @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) {
      // 1. 如果此時請求被取消了,那麼關閉連接,釋放資源
      if (canceled) { 
        streamAllocation.release();
        throw new IOException("Canceled");
      }

      Response response;
      boolean releaseConnection = true;
      try {
        // 2. 推進執行攔截器鏈,請求並返回響應數據
        response = realChain.proceed(request, streamAllocation, null, null);
        releaseConnection = false;
      } catch (RouteException e) {
        // 3. 如果連接失敗了,嘗試使用失敗的地址恢復一下,此時請求可能還沒有發送
        if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
          throw e.getFirstConnectException();
        }
        releaseConnection = false;
        continue;
      } catch (IOException e) {
        // 4. 嘗試重新與交流失敗的服務器重新交流,這個時候請求可能已經發送了
        boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
        if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
        releaseConnection = false;
        continue;
      } finally {
        // 5. 如果是位置異常,那麼釋放掉所有資源
        if (releaseConnection) {
          streamAllocation.streamFailed(null);
          streamAllocation.release();
        }
      }
      // 6. 如果記錄的上一個請求大的響應數據存在,那麼將其響應體置空
      if (priorResponse != null) {
        response = response.newBuilder()
            .priorResponse(priorResponse.newBuilder()
                    .body(null)
                    .build())
            .build();
      }

      Request followUp;
      try {
        // 7. 處理請求的認證頭部、重定向或請求超時問題,如果這些操作都不必要
        //    或者應用不了,那麼返回 null
        followUp = followUpRequest(response, streamAllocation.route());
      } catch (IOException e) {
        streamAllocation.release();
        throw e;
      }
      //需要處理認證、重定向和超時問題,那麼結束處理,返回響應數據
      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 (!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;
    }
  }

2). BridgeInterceptor

作用:應用代碼與網絡代碼的橋樑。首先根據用戶請求建立網絡請求,然後執行這個網絡請求,最後根據網絡請求的響應數據建立一個用戶響應數據。

BridgeInterceptor#intercept源碼如下,主要做了以下事情:

  • 用於請求: 這個是在推進請求攔截器鏈時進行的,也就是說此時尚未真正地進行網絡請求。此時會補充缺失的請求頭參數,如 Content-TypeTransfer-EncodingHostConnectionAccept-EncodingUser-AgentCookie。如果在請求時添加了gzip請求頭參數,即開啓了gzip壓縮,那麼在取得響應數據時需要對數據進行解壓。
  • 用於響應: 這個實在取得網絡響應數據後回退攔截器鏈時進行的,即已經取得了網絡響應數據。此時會對相應頭部進行處理,如果請求時開啓了gzip壓縮,那麼此時會對響應數據進行解壓。
  @Override public Response intercept(Chain chain) throws IOException {
    Request userRequest = chain.request();
    Request.Builder requestBuilder = userRequest.newBuilder();

    // 這裏處於請求之前
    // 1. 此時主要爲請求添加缺失的請求頭參數
    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");
      }
    }
    // ...
    // 如果啓用了GZIP壓縮,那麼需要負責解壓響應數據
    boolean transparentGzip = false;
    if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
      transparentGzip = true;
      requestBuilder.header("Accept-Encoding", "gzip");
    }
    //...

    // 2. 推進執行攔截器鏈,進行請求、返回數據
    Response networkResponse = chain.proceed(requestBuilder.build());

    // 取得網絡響應數據後
    // 3. 處理響應頭,如果請求時開啓了GZIP壓縮,那麼這裏需要將響應數據解壓
    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();
  }

綜上所述,BridgeInterceptor主要用於請求前對用戶請求進行完善,補充缺失參數,然後推進請求攔截器鏈,並等待響應數據返回,取得響應數據後則是將其轉換成用戶響應數據,此時如果數據進行過gzip壓縮,那麼會在這裏進行解壓,然後重新封裝成用戶數據。

3). CacheInterceptor

作用:用於從本地緩存中讀取數據,以及將服務器數據寫入到緩存。

CacheInterceptor#intercept源碼如下,攔截器鏈執行到這一步主要做了如下事情:

  • 請求:

    如果開啓了緩存,且請求策略是禁用網絡僅讀緩存的話,那麼首先會根據當前請求去查找緩存,如果匹配到了緩存,則將緩存封裝成響應數據返回,如果沒有匹配到,那麼返回一個504的響應,這將導致請求攔截器鏈執行終止,進而返回執行響應攔截器鏈。

    如果請求策略是網絡加緩存,當那麼然網絡請求優先,所以就推進請求攔截器鏈執行請求,

  • 網絡響應:

    在得到網絡響應數據後,如果開啓了緩存策略其匹配到了舊緩存,那麼根據最新網絡請求響應數據更新緩存,然後返回響應數據;如果沒有匹配到緩存但是開啓了緩存,那麼將響應數據寫入緩存後返回;而如果開啓了緩存,但是並不使用緩存策略,那麼根據響應數據移除緩存中對應的數據緩存。

  @Override public Response intercept(Chain chain) throws IOException {
    //  讀取候選的舊緩存
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;

    long now = System.currentTimeMillis();
    // 解析請求和緩存策略
    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    Request networkRequest = strategy.networkRequest; // 如果僅讀緩存,那麼網絡請求會爲 null
    Response cacheResponse = strategy.cacheResponse;
    // ... 
    // 如果禁止使用網絡而僅讀取緩存的話,那麼沒匹配到緩存時返回 504
    if (networkRequest == null && cacheResponse == null) {
      return new Response.Builder()
          // ... 
          .code(504)
          .message("Unsatisfiable Request (only-if-cached)")
          // ...
          .body(Util.EMPTY_RESPONSE)
          .build();
    }

    // 如果禁止使用網絡而僅讀取緩存的話,那麼匹配到緩存時將其返回
    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }

    Response networkResponse = null;
    try {
      // 推進執行請求攔截器鏈
      networkResponse = chain.proceed(networkRequest);
    } finally {
      // 請求異常則關閉候選緩存實體
      if (networkResponse == null && cacheCandidate != null) {
        closeQuietly(cacheCandidate.body());
      }
    }

    // 根據最新網絡請求響應數據更新緩存,然後返回響應數據
    if (cacheResponse != null) {
      if (networkResponse.code() == HTTP_NOT_MODIFIED) {
        Response response = cacheResponse.newBuilder()
            // ...
            .cacheResponse(stripBody(cacheResponse))
            .networkResponse(stripBody(networkResponse))
            .build();
        networkResponse.body().close();
        // ...
        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)) {
        // 使用緩存策略且無匹配緩存,則將響應數據寫入緩存
        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;
  }

緩存時判斷邏輯比較多,不過這裏重點在於理解緩存策略,一般會有:僅網絡僅緩存網絡加緩存 三種請求策略。

4). ConnectInterceptor

作用:用於打開一個到目標服務器的連接,並切換至下一個攔截器

因爲在上一節末尾分析OkHttp如何建立連接的問題上已經分析過了,所以不做過多描述。

這裏回憶一下連接規則:建立連接時,首先會查看當前是否有可用連接,如果沒有,那麼會去連接池中查找,如果找到了,當然就使用這個連接,如果沒有,那麼就新建一個連接,進行TCPTLS握手以建立聯繫,接着把連接放入連接池以備後續複用,最後推進請求攔截器鏈執行,將打開的連接交給下一個攔截器去處理。

5). CallServerInterceptor

作用:這是攔截器鏈的最後一環,至此將真正的進行服務器請求

CallServerInterceptor#intercept源碼如下,作爲攔截器鏈的最後一環,當然要真正地做點實事了,大致操作步驟是:

發送請求頭部 --> 讀取一下GETHEAD之外的請求(如POST)的響應數據 --> 結束請求的發送動作 --> 讀取響應頭部 --> 讀取響應數據 --> 封裝後返回。

  @Override public Response intercept(Chain chain) throws IOException {
    // ...
    long sentRequestMillis = System.currentTimeMillis();

    // 1. 發送請求頭部
    httpCodec.writeRequestHeaders(request);

    Response.Builder responseBuilder = null;
    // 2. 檢查是否是 GET 或 HEAD 以外的請求方式、讀取響應數據
    if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
      // 如果請求頭部中包含"Expect: 100-continue",那麼在轉換請求體前等待"HTTP/1.1 100 Continue"
      // 響應。如果沒有讀取到"HTTP/1.1 100 Continue"的響應,那麼就不轉換請求體(request body)了,
      // 而直接將我們得到的響應返回(如 狀態爲 4XX 的響應 );
      if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
        httpCodec.flushRequest();
        // 讀取、轉換響應頭部
        responseBuilder = httpCodec.readResponseHeaders(true);
      }

      if (responseBuilder == null) {
        // 如果存在"Expect: 100-continue",則寫入請求體
        long contentLength = request.body().contentLength();
        CountingSink requestBodyOut =
            new CountingSink(httpCodec.createRequestBody(request, contentLength));
        BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);

        request.body().writeTo(bufferedRequestBody);
        bufferedRequestBody.close();
      } else if (!connection.isMultiplexed()) {
        // 如果不存在"Expect: 100-continue",那麼禁止 HTTP/1 連接複用。
        // 不過我們仍然必須轉換請求體以使連接達到一個固定的狀態。
        streamAllocation.noNewStreams();
      }
    }
    // 3. 結束請求的發送動作
    httpCodec.finishRequest();


    // 4. 讀取響應頭部
    if (responseBuilder == null) {
      responseBuilder = httpCodec.readResponseHeaders(false);
    }

    // 5. 構建響應數據
    Response response = responseBuilder
        .request(request)
        .handshake(streamAllocation.connection().handshake())
        .sentRequestAtMillis(sentRequestMillis)
        .receivedResponseAtMillis(System.currentTimeMillis())
        .build();

    int code = response.code();
    if (code == 100) {
      // 如果服務器返回 100-continue 響應即使我們並沒有這麼請求,則重新讀取一遍響應數據;
      responseBuilder = httpCodec.readResponseHeaders(false);

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

      code = response.code();
    }
    // 6. 填充響應數據
    if (forWebSocket && code == 101) {
      // 連接正在更新,不過我們需要確保攔截器收到的 non-null 的響應體
      response = response.newBuilder()
          .body(Util.EMPTY_RESPONSE)
          .build();
    } else {
      // 這裏將 http 輸入流包裝到響應體
      response = response.newBuilder()
          .body(httpCodec.openResponseBody(response))
          .build();
    }
    // 其他情況...
    return response;
  }

綜上,作爲攔截器最後一環的CallServerInterceptor終於把請求給終結了,完成了與服務器的溝通交流,把需要的數據拿了回來。請求的時候每個攔截器都會插上一腳,響應的時候也一樣,把數據轉換的工作分給了各個攔截器處理。

2. 分發器(Dispatcher

爲什麼叫分發器呢?如果叫做執行器(Executor)可能會更好理解一些,因爲它的工作就是執行異步請求,雖然會統計請求的數量....嗯~~好吧,換個角度,如果理解爲它用於把異步任務分發給線程池執行,起到任務分發的作用,那就理解爲啥叫分發器了。

OK,先來觀察一下Dispatcher的構成,部分源碼如下,可以先看看註釋:

public final class Dispatcher {
  private int maxRequests = 64;  // 同時執行的最大異步請求數量,數量超過該值時,新增的請求會放入異步請求隊列中
  private int maxRequestsPerHost = 5;  // 每個主機最多同時存在的請求數量
  private @Nullable Runnable idleCallback; 
 
  // 線程池執行器
  private @Nullable ExecutorService executorService;
  // 尚未執行的任務隊列
  private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
  // 正在執行的任務隊列
  private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
  // 同步執行的任務隊列
  private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();

  public Dispatcher(ExecutorService executorService) {
    this.executorService = executorService;
  }
  
  public synchronized ExecutorService executorService() {
    if (executorService == null) {
      // 初始化線程池執行器
      executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
          new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
    }
    return executorService;
  }
  // ...
}

簡單描述一下:Dispatcher包含三個任務隊列,分別用於記錄尚未執行的異步請求、正在執行的異步請求、正在執行的同步請求。包含一個線程池,用於執行異步請求,這個線程池執行器的核心線程數量爲 0, 最大線程數量不限(整型的最大值2^31-1,相當於不限),閒置線程的最大等待超時時間爲60秒,線程池的任務隊列使用非公平機制的SynchronousQueue。這就是Dispatcher的主要配置。

我們來看看它是如何限制每個主機的請求數量的,直接看註釋好了。

synchronized void enqueue(AsyncCall call) {
    // 正在執行的異步請求數量小於限定值,且同一主機的正在執行的異步請求數量小於限定值時
    // 添加到正在執行的異步請求隊列中,並執行。
    if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
      runningAsyncCalls.add(call);
      executorService().execute(call);
    } else { // 否則就添加到等待隊列中
      readyAsyncCalls.add(call);
    }
  }

runningCallsForHost用於計算當前正在執行的連接到相同主機上異步請求的數量

 private int runningCallsForHost(AsyncCall call) {
    int result = 0;
    for (AsyncCall c : runningAsyncCalls) {
      if (c.get().forWebSocket) continue;
      if (c.host().equals(call.host())) result++;
    }
    return result;
  }

3. 連接池(ConnetionPool

作用:用於管理HTTPHTTP/2連接的複用以減少網絡延遲,因爲使用相同地址的請求可能共享一個連接。所以ConnetionPool實現了維護已打開連接已被後續使用的機制。

線程池啥的就不多說了,這裏主要分析一下ConnetionPool如何維護已打開的連接。從ConnetionPool#put着手:

  void put(RealConnection connection) {
    assert (Thread.holdsLock(this));
    if (!cleanupRunning) { // 如果當前不在執行清理任務,那麼現在執行
      cleanupRunning = true;
      executor.execute(cleanupRunnable); // 線程池的作用就是執行清理任務
    }
    connections.add(connection); // 同時添加到連接隊列中
  }

cleanupRunnable源碼如下,根據ConnetionPool#put可知每當我們往連接池中添加一個連接時,如果當前不在執行清理任務(cleanupRunnable),那麼立馬會執行cleanupRunnable,而cleanupRunnable中會循環執行cleanup,直到所有連接都因閒置超時而被清理掉,具體還請先看註釋。

private final Runnable cleanupRunnable = new Runnable() {
    @Override public void run() {
      while (true) {
        long waitNanos = cleanup(System.nanoTime()); // 執行清理
        if (waitNanos == -1) return; // cleanup中的情況 4)
        if (waitNanos > 0) {  // cleanup中的情況 2) 和 3)  
          long waitMillis = waitNanos / 1000000L;
          waitNanos -= (waitMillis * 1000000L);
          synchronized (ConnectionPool.this) {
            try {
              ConnectionPool.this.wait(waitMillis, (int) waitNanos); // 等待超時
            } catch (InterruptedException ignored) {
            }
          }
        }
        // 至此情況 1), 2), 3) 都會導致 `cleanup`被循環執行
      }
    }
  };

cleanup源碼如下,它的作用是查找、清理超過了keep-alive時間限制或者閒置超時閒置的連接。具體還請看註釋。

long cleanup(long now) {
    int inUseConnectionCount = 0;
    int idleConnectionCount = 0;
    RealConnection longestIdleConnection = null;
    long longestIdleDurationNs = Long.MIN_VALUE;

    // Find either a connection to evict, or the time that the next eviction is due.
    synchronized (this) {
      // 1. 查找超時連接
      for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
        RealConnection connection = i.next();
        // 1). 如果正在使用,那麼跳過,繼續查找
        if (pruneAndGetAllocationCount(connection, now) > 0) {
          inUseConnectionCount++;
          continue;
        }

        idleConnectionCount++; // 記錄閒置連接的數量

        // 2). 如果閒置時間超過了最大允許閒置時間,則記錄下來在後面清除
        long idleDurationNs = now - connection.idleAtNanos;
        if (idleDurationNs > longestIdleDurationNs) {
          longestIdleDurationNs = idleDurationNs;
          longestIdleConnection = connection;
        }
      }

      // 2. 查找完成了,將在這裏對閒置連接進行處理
      if (longestIdleDurationNs >= this.keepAliveDurationNs
          || idleConnectionCount > this.maxIdleConnections) {
        // 1). 確定已經超時了,那麼從連接池中清除,關閉動作會在同步塊外面進行
        connections.remove(longestIdleConnection);
      } else if (idleConnectionCount > 0) {
        // 2). 存在閒置連接,但是尚未超時
        return keepAliveDurationNs - longestIdleDurationNs;
      } else if (inUseConnectionCount > 0) {
        // 3). 如果所有連接都正在使用,那麼最多保持個`keep-alive`超時時間就又會重新執行清理動作
        return keepAliveDurationNs;
      } else {
        // 4). 壓根沒有連接,那不管了,標記爲非清理狀態,並返回-1
        cleanupRunning = false;
        return -1;
      }
    }
    // 3. 關閉上面查找到的處於情況1)的閒置超時連接
    closeQuietly(longestIdleConnection.socket());
    // 返回 0 ,表示馬上會重新回來執行清理操作
    return 0;
  }

綜上,ConnectionPool 會設定keepAliveDurationNslongestIdleDurationNs兩個超時時間,而每次往連接池中添加一個新連接時,如果當前處於非清理裝填,都會導致線程池執行器開個線程執行清理動作,而對於清理動作而言,會遍歷連接池,查找閒置超時的連接,並記錄閒置連接的數量,而遍歷完成後,將根據情況 2. 1)、2)、3)、4) 進行相應的處理,而如果是情況 2. 4), 則會當即結束清理循環,意味着連接池中已經沒有連接了,此時線程會執行完成而退出,其他幾種情況都不會中斷循環,因此實際上這個線程池最多隻會存在一個連接池維護線程。

四、總結

一般來說,當使用OKHttp通過URL請求時,它做了以下事情:

  • 使用URL並且配置OKHttpClient來創建地址(Address),這個地址指定了我們連接web服務器的方式。
  • 嘗試從連接池(ConnectionPool)中取出與該地址相同的連接。
  • 如果在連接池中沒有對應該地址的連接,那麼它會選擇一條新路線(route)去嘗試,這通常意味着將進行DNS請求以獲取對應服務器的IP地址,然後如果需要的話還會選擇TLS版本和代理服務器。
  • 如果這是一條新路線,它會通過Socket連、TLS隧道(HTTP代理的HTTPS)或者TLS連接,然後根據需要進行TCPTLS握手。
  • 發送HTTP請求,然後讀取響應數據。

如果連接出現問題,OKHttp會選擇另一條路線再次嘗試,這使得OKHttp在服務器地址子集無法訪問時能夠恢復,而當從連接池中拿到的連接已經過期,或者TLS版本不支持的情況下,這種方式同樣很有用。一旦接收到響應數據,該連接就會返回到連接池中以備後續請求使用,而連接池中的連接也會在一定時間的不活動狀態後被清除掉。

對於整體框架而言,本文已經詳細分析了OkHttp的整體工作流程,相關細節還請回到文中去,這裏就不再累贅了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章