Android小知識-剖析OkHttp中的五個攔截器(下篇)

公眾號_顧林海發表於2018-10-28

本平臺的文章更新會有延遲,大家可以關注微信公眾號-顧林海,包括年底前會更新kotlin由淺入深系列教程,目前計劃在微信公眾號進行首發,如果大家想獲取最新教程,請關注微信公眾號,謝謝

在上一節介紹了快取攔截器CacheInterceptor的快取機制,內部採用DiskLruCache來快取資料,本節介紹剩下的兩個攔截器,分別是ConnectInterceptor和CallServerInterceptor攔截器。

ConnectInterceptor攔截器

ConnectInterceptor是網路連線攔截器,我們知道在OkHttp當中真正的網路請求都是通過攔截器鏈來實現的,通過依次執行這個攔截器鏈上不同功能的攔截器來完成資料的響應,ConnectInterceptor的作用就是開啟與伺服器之間的連線,正式開啟OkHttp的網路請求。

走進ConnectInterceptor的intercept方法:

    @Override public Response intercept(Interceptor.Chain chain) throws IOException {
        RealInterceptorChain realChain = (RealInterceptorChain) chain;
        Request request = realChain.request();
        //標記1
        StreamAllocation streamAllocation = realChain.streamAllocation();
        ...
    }
複製程式碼

在標記1處可以看到從上一個攔截器中獲取StreamAllocation物件,在講解第一個攔截器RetryAndFollowUpInterceptor重試重定向的時候已經介紹過StreamAllocation,在RetryAndFollowUpInterceptor中只是建立了這個物件並沒有使用,真正使用它的是在ConnectInterceptor中,StreamAllocation是用來建立執行HTTP請求所需要的網路元件,既然我們拿到了StreamAllocation,接下來看這個StreamAllocation到底做了哪些操作。

    @Override public Response intercept(Interceptor.Chain chain) throws IOException {
        ...
        //標記1
        StreamAllocation streamAllocation = realChain.streamAllocation();
        ..
        //標記2
        HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
        ...
    }
複製程式碼

在標記2處通過StreamAllocation物件的newStream方法建立了一個HttpCodec物件,HttpCodec的作用是用來編碼我們的Request以及解碼我們的Response。

    @Override public Response intercept(Interceptor.Chain chain) throws IOException {
        ...
        //標記1
        StreamAllocation streamAllocation = realChain.streamAllocation();
        ...
        //標記2
        HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
        //標記3
        RealConnection connection = streamAllocation.connection();
        ...
    }
複製程式碼

在標記3處通過StreamAllocation物件的connection方法獲取到RealConnection物件,這個RealConnection物件是用來進行實際的網路IO傳輸的。

    @Override public Response intercept(Interceptor.Chain chain) throws IOException {
        ...
        //標記1
        StreamAllocation streamAllocation = realChain.streamAllocation();
        ...
        //標記2
        HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
        //標記3
        RealConnection connection = streamAllocation.connection();
        //標記4
        return realChain.proceed(request, streamAllocation, httpCodec, connection);
    }
複製程式碼

標記4處是我們非常熟悉的程式碼了,繼續呼叫攔截器鏈的下一個攔截器並將Request、StreamAllocation、HttpCodec以及RealConnection物件傳遞過去。

總結: 首先ConnectInterceptor攔截器從攔截器鏈獲取到前面傳遞過來的StreamAllocation,接著執行StreamAllocation的newStream方法建立HttpCodec,HttpCodec物件是用於處理我們的Request和Response。 最後將剛才建立的用於網路IO的RealConnection物件,以及對於伺服器互動最為關鍵的HttpCodec等物件傳遞給後面的攔截器。

從上面我們瞭解了ConnectInterceptor攔截器的intercept方法的整體流程,從前一個攔截器中獲取StreamAllocation物件,通過StreamAllocation物件的newStream方法建立了一個HttpCodec物件,我們看看這個newStream方法具體做了哪些操作。

    public HttpCodec newStream(
            OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
        ...
        try {
            //標記1
            RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
                    writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
            HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);
            ...
        } catch (IOException e) {
            throw new RouteException(e);
        }
    }
複製程式碼

我們可以看到在標記1處建立了一個RealConnection物件,以及HttpCodec物件,這兩個物件在上面已經介紹過了,RealConnection物件是用來進行實際的網路IO傳輸的,HttpCodec是用來編碼我們的Request以及解碼我們的Response。

通過findHealthyConnection方法生成一個RealConnection物件,來進行實際的網路連線。

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);
            ...
            synchronized (connectionPool) {
                if (candidate.successCount == 0) {
                    return candidate;
                }
            }
            ...
            return candidate;
        }
    }
複製程式碼

在方法中開啟了while迴圈,內部的同步程式碼塊中判斷candidate的successCount如果等於0,說明整個網路連線結束並直接返回candidate,而這個candidate是通過同步程式碼塊上面的findConnection方法獲取的。

    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);
            synchronized (connectionPool) {
                if (candidate.successCount == 0) {
                    return candidate;
                }
            }
            //標記1
            if (!candidate.isHealthy(doExtensiveHealthChecks)) {
                noNewStreams();
                continue;
            }
            return candidate;
        }
    }
複製程式碼

往下看標記1,這邊會判斷這個連線是否健康(比如Socket沒有關閉、或者它的輸入輸出流沒有關閉等等),如果不健康就呼叫noNewStreams方法從連線池中取出並銷燬,接著呼叫continue,繼續迴圈呼叫findConnection方法獲取RealConnection物件。

通過不停的迴圈呼叫findConnection方法來獲取RealConnection物件,接著看這個findConnection方法做了哪些操作。

    private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
                                          int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
        ...
        RealConnection result = null;
        Connection releasedConnection;
        ...
        synchronized (connectionPool) {
            ...
            //標記1
            releasedConnection = this.connection;
            ...
            if (this.connection != null) {
                result = this.connection;
                releasedConnection = null;
            }
            ...
        }
        ...
        return result;
    }
複製程式碼

在findConnection方法的標記1處,嘗試將connection賦值給releasedConnection,然後判斷這個connection能不能複用,如果能複用,就將connection賦值給result,最後返回這個複用的連線。如果不能複用,那麼result就為null,我們繼續往下看。

   private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
                                          int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
        ...
        RealConnection result = null;
        Connection releasedConnection;
        ...
        synchronized (connectionPool) {
            ...
            //標記1
            ...
            //標記2
            if (result == null) {
                Internal.instance.get(connectionPool, address, this, null);
                if (connection != null) {
                    foundPooledConnection = true;
                    result = connection;
                } else {
                    selectedRoute = route;
                }
            }
            ...
        }
        ...
        return result;
    }
複製程式碼

如果result為null說明不能複用這個connection,那麼就從連線池connectionPool中獲取一個實際的RealConnection並賦值給connection,接著判斷connection是否為空,不為空賦值給result。

    private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
                                          int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
        ...
        RealConnection result = null;
        Connection releasedConnection;
        ...
        synchronized (connectionPool) {
            ...
            //標記1
            ...
            //標記2
            if (result == null) {
                Internal.instance.get(connectionPool, address, this, null);
                if (connection != null) {
                    foundPooledConnection = true;
                    result = connection;
                } else {
                    selectedRoute = route;
                }
            }
            ...
        }
        ...
        //標記3
        result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
                connectionRetryEnabled, call, eventListener);
        ...
        return result;
    }
複製程式碼

標記3處,拿到我們的RealConnection物件result之後,呼叫它的connect方法來進行實際的網路連線。

    private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
                                          int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
        ...
        RealConnection result = null;
        Connection releasedConnection;
        ...
        synchronized (connectionPool) {
            ...
            //標記1
            ...
            //標記2
            if (result == null) {
                Internal.instance.get(connectionPool, address, this, null);
                if (connection != null) {
                    foundPooledConnection = true;
                    result = connection;
                } else {
                    selectedRoute = route;
                }
            }
            ...
        }
        ...
        //標記3
        result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
                connectionRetryEnabled, call, eventListener);
        ...
        //標記4
        Internal.instance.put(connectionPool, result);
        ...
        return result;
    }
複製程式碼

在標記4處,進行真正的網路連線後,將連線成功後的RealConnection物件result放入connectionPool連線池,方便後面複用。

上面我們介紹了StreamAllocation物件的newStream方法的具體操作,接下來看看ConnectInterceptor攔截器中一個很重要的概念-連線池。

不管HTTP協議是1.1還是2.0,它們的Keep-Alive機制,或者2.0的多路複用機制在實現上都需要引入一個連線池的概念,來維護整個網路連線。OkHttp中將客戶端與服務端之間的連結抽象成一個Connection類,而RealConnection是它的實現類,為了管理所有的Connection,OkHttp提供了一個ConnectionPool這個類,它的主要作用就是在時間範圍內複用Connection。

接下來主要介紹它的get和put方法。

    @Nullable RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
        assert (Thread.holdsLock(this));
        for (RealConnection connection : connections) {
            if (connection.isEligible(address, route)) {
                streamAllocation.acquire(connection, true);
                return connection;
            }
        }
        return null;
    }
複製程式碼

在get方法中遍歷連線池中的Connection,通過isEligible方法判斷Connection是否可用,如果可以使用就會呼叫streamAllocation的acquire方法來獲取所用的連線。

進入StreamAllocation的acquire方法:

  public void acquire(RealConnection connection, boolean reportedAcquired) {
    assert (Thread.holdsLock(connectionPool));
    if (this.connection != null) throw new IllegalStateException();
    //標記1
    this.connection = connection;
    this.reportedAcquired = reportedAcquired;
    //標記2
    connection.allocations.add(new StreamAllocationReference(this, callStackTrace));
  }
複製程式碼

標記1處,從連線池中獲取的RealConnection物件賦值給StreamAllocation的成員變數connection。

標記2處,將StreamAllocation物件的弱引用新增到RealConnection的allocations集合中去,這樣做的用處是通過allocations集合的大小來判斷網路連線次數是否超過OkHttp指定的連線次數。

put方法:

  void put(RealConnection connection) {
    assert (Thread.holdsLock(this));
    if (!cleanupRunning) {
      cleanupRunning = true;
      executor.execute(cleanupRunnable);
    }
    connections.add(connection);
  }
複製程式碼

put方法中在新增連線到連線池之前,會處理清理任務,做完清理任務後,將我們的connection新增到連線池中。

connection自動回收l利用了GC的回收演算法,當StreamAllocation數量為0時,會被執行緒池檢測到,然後進行回收,在ConnectionPool中有一個獨立的執行緒,它會開啟cleanupRunnable來清理連線池。

  private final Runnable cleanupRunnable = new Runnable() {
    @Override public void run() {
      while (true) {
         //標記1
        long waitNanos = cleanup(System.nanoTime());
        if (waitNanos == -1) return;
        if (waitNanos > 0) {
          long waitMillis = waitNanos / 1000000L;
          waitNanos -= (waitMillis * 1000000L);
          synchronized (ConnectionPool.this) {
            try {
              //標記2
              ConnectionPool.this.wait(waitMillis, (int) waitNanos);
            } catch (InterruptedException ignored) {
            }
          }
        }
      }
    }
  };
複製程式碼

在run方法中是一個死迴圈,內部標記1處首次進行清理時,需要返回下次清理的間隔時間。標記2處呼叫了wait方法進行等待,等待釋放鎖和時間片,當等待時間過了之後會再次呼叫Runnable進行清理,同時返回下次要清理的間隔時間waitNanos。

標記2處的cleanup方法內部實現了具體的GC回收演算法,該演算法類似Java GC當中的標記清除演算法;cleanup方法迴圈標記出最不活躍的connection,通過響應的判斷來進行清理。

CallServerInterceptor攔截器

CallServerInterceptor攔截器主要作用是負責向伺服器發起真正的網路請求,並獲取返回結果。

CallServerInterceptor的intercept方法:

  @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();
    ...
}
複製程式碼

intercept方法中先是獲取五個物件,下面分別介紹這5個物件的含義。

  • RealInterceptorChain:攔截器鏈,真正進行請求的地方。

  • HttpCodec:在OkHttp中,它把所有的流物件都封裝成了HttpCodec這個類,作用是編碼Request,解碼Response。

  • StreamAllocation:建立HTTP連線所需要的網路元件。

  • RealConnection:伺服器與客戶端的具體連線。

  • Request:網路請求。

  @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();
    ...
    //標記1
    httpCodec.finishRequest();
    ...
}
複製程式碼

標記1處,呼叫httpCodec的finishRequest方法,表面網路請求的寫入工作已經完成,具體網路請求的寫入工作大家可以看原始碼,也就是標記1之上的程式碼。

網路請求的寫入工作完成後,接下來就進行網路請求的讀取工作。

  @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();
    ...
    //網路請求一系列寫入工作
    ...
    //向socket當中寫入請求的body資訊
    request.body().writeTo(bufferedRequestBody);
    ...
    //標記1:寫入結束
    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
    //標記1
    response = response.newBuilder()
          .body(httpCodec.openResponseBody(response))
          .build();
    ...
    return response;
}
複製程式碼

我們只取核心程式碼標記1,通過httpCodec的openResponseBody方法獲取body,並通過build建立Response物件,最終返回Response物件。

到這裡OkHttp的同步和非同步請求、分發器,以及五個攔截器都已經介紹一邊了,怎麼說呢,OkHttp的原始碼實在太龐大了,要想全部理解需要花費很長時間,我只是整理出了OkHttp中幾個比較重要的概念,瞭解它的整體脈絡,這樣你才能有條理的分析它的原始碼。


838794-506ddad529df4cd4.webp.jpg

搜尋微信“顧林海”公眾號,定期推送優質文章。

相關文章