從源碼的角度分析 OKHttp3 (二) 攔截器的魅力

前言

因爲以前項目搭建的是 MVP 架構,由RxJava + Glide + OKHttp + Retrofit 等開源框架組合而成,以前也都是停留在使用層面上,沒有深刻的研究,最近打算把它們所有攻下,尚未關注的同窗能夠先關注一波,看完這個系列文章,(無論是面試仍是工做中處理問題)相信你都在知道原理的狀況下,處理問題更加駕輕就熟。html

Android 圖片加載框架 Glide 4.9.0 (一) 從源碼的角度分析 Glide 執行流程java

Android 圖片加載框架 Glide 4.9.0 (二) 從源碼的角度分析 Glide 緩存策略git

從源碼的角度分析 Rxjava2 的基本執行流程、線程切換原理github

從源碼的角度分析 OKHttp3 (一) 同步、異步執行流程web

從源碼的角度分析 OKHttp3 (二) 攔截器的魅力面試

從源碼的角度分析 OKHttp3 (三) 緩存策略緩存

從源碼的角度分析 Retrofit 網絡請求,包含 RxJava + Retrofit + OKhttp 網絡請求執行流程服務器

interceptor 攔截器

在上一篇 從源碼的角度分析 OKHttp3 (一) 同步、異步執行流程 文章中,最後咱們知道是在 getResponseWithInterceptorChain() 函數中完成了最後的請求與響應,那麼內部是怎麼完成請求,並把服務端的響應數據回調給調用層,先來看一段代碼:cookie

Response getResponseWithInterceptorChain() throws IOException {
    // 構建一個攔截器調用的容器棧
    List<Interceptor> interceptors = new ArrayList<>();
    //配置 OKHttpClient 的時候,以 addInterceptor 方式添加的全局攔截器
    interceptors.addAll(client.interceptors());
    //錯誤、重定向攔截器
    interceptors.add(new RetryAndFollowUpInterceptor(client));
    //橋接攔截器,橋接應用層與網絡層,添加必要的頭
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    //緩存處理,Last-Modified、ETag、DiskLruCache等
    interceptors.add(new CacheInterceptor(client.internalCache()));
    //鏈接攔截器
    interceptors.add(new ConnectInterceptor(client));
    //是不是 webSocket
    if (!forWebSocket) {
    //經過okHttpClient.Builder#addNetworkInterceptor()
    //傳進來的攔截器只對非網頁的請求生效
      interceptors.addAll(client.networkInterceptors());
    }
    //真正訪問服務器的攔截器
    interceptors.add(new CallServerInterceptor(forWebSocket));
		
    //真正執行 攔截器的 調用者
    Interceptor.Chain chain = new RealInterceptorChain(interceptors, transmitter, null, 0,
        originalRequest, this, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());

    boolean calledNoMoreExchanges = false;
    try {
      //開始執行
      Response response = chain.proceed(originalRequest);
      //是否取消
      if (transmitter.isCanceled()) {
        //關閉
        closeQuietly(response);
        throw new IOException("Canceled");
      }
      return response;
    } catch (IOException e) {
      calledNoMoreExchanges = true;
      throw transmitter.noMoreExchanges(e);
    } finally {
      if (!calledNoMoreExchanges) {
        transmitter.noMoreExchanges(null);
      }
    }
  }
複製代碼

函數中代碼很少,但確實精髓所在。經過上面代碼跟註釋咱們知道網絡

  1. 首先建立一個用來裝攔截器的容器
  2. 添加全局攔截器跟應用攔截器
  3. 建立 RealInterceptorChain 對象攔截器,並把 攔截器容器、發射器、請求數據等一些配置傳入進去
  4. 最後調用 RealInterceptorChainchain.proceed(originalRequest); 函數, 纔是真正使 這些攔截器執行起來。

上一篇文章也簡單的介紹了攔截器,提到了 責任鏈模式,可是這個攔截器它是經過 RealInterceptorChain 對象開啓了責任鏈任務的下發,這裏感受是否是有點像一個 CEO 在下發任務並一層一層的傳遞,也有點像 Android 源碼中 觸摸反饋事件傳遞,OKHttp 的核心其實就在於攔截器。下面咱們就開始一步一步分析 OKHttp 攔截器的精妙所在。

RealInterceptorChain

經過上一小節對攔截器的介紹,咱們知道最後是在 RealInterceptorChainchain.proceed(originalRequest) 開啓執行的攔截任務,下面直接進入源碼模式

public final class RealInterceptorChain implements Interceptor.Chain {
	...//省略成員變量屬性

  public RealInterceptorChain( List<Interceptor> interceptors, //全部攔截器 Transmitter transmitter,//發射器 @Nullable Exchange exchange, //封裝對 OKIO 的請求數據的操做 int index, Request request, Call call, int connectTimeout, int readTimeout, int writeTimeout ){
	...//省略賦值代碼
  }
  
  //外部 getResponseWithInterceptorChain 函數中調用
  public Response proceed( Request request, Transmitter transmitter, @Nullable Exchange exchange )throws IOException {
    
    //index 不能超過攔截器容器大小
    if (index >= interceptors.size()) throw new AssertionError();

 		//若是已經存在了一個 request 的請求鏈接就拋一個異常
    if (this.exchange != null && !this.exchange.connection().supportsUrl(request.url())) {
      ...//拋異常代碼省略
    }

    // 保證開啓調用的惟一性,不然拋一個異常,我的認爲這樣判斷只是使得代碼更加健壯,其實這裏的 Calls 只會是 1;
    if (this.exchange != null && calls > 1) {
       ...//拋異常代碼省略
    }

    //1. 建立下一個攔截器執行的對象
    RealInterceptorChain next = new RealInterceptorChain(interceptors, transmitter, exchange,
        index + 1, request, call, connectTimeout, readTimeout, writeTimeout);
    //2. 取出當前的攔截器
    Interceptor interceptor = interceptors.get(index);
    //3. 調用下一個攔截器的 intercept(Chain) 方法,傳入剛纔新建的 RealInterceptorChain, //返回 Response
    Response response = interceptor.intercept(next);

		//限制作一些判斷,保證程序健壯
    if (exchange != null && index + 1 < interceptors.size() && next.calls != 1) {
      ...//拋異常代碼省略
    }

		//若是返回回來的 response 爲空,那麼就拋一個異常
    if (response == null) {
      ...//拋異常代碼省略
    }

    //若是響應爲空,也拋出一個異常
    if (response.body() == null) {
      ...//拋異常代碼省略
    }
    
    //真正返回服務端的響應
    return response;
  }
  
}
複製代碼

請看上面代碼註釋 1,2,3 處,這三處代碼就是分發攔截器執行的核心代碼,首先看註釋一 在 RealInterceptorChain 內部又建立一個 RealInterceptorChain 並傳入 index + 1 等參數, 這裏就是開始遞歸執行攔截器了,每次執行 get(index + 1)攔截器。註釋 2 是取出當前攔截器,註釋三是執行攔截器。

這裏咱們能夠先小節總結下RealInterceptorChain 的做用, 能夠把 RealInterceptorChain 這個類看作看一個遞歸函數 interceptor.intercept(next); 就是開始遞歸的入口,固然有入口確定有出口,其實出口沒有在這個類裏面,這裏我先透露下吧,實際上是在 CallServerInterceptor 請求與響應處理的攔截器中,最後直接return response;至關於出口。因此 RealInterceptorChain 這個類我的理解就是負責 啓動/中止 攔截器的做用,有點像攔截器的調用委託於 RealInterceptorChain 。

那麼這裏確定是 list.get(index = 0) RetryAndFollowUpInterceptor 攔截器第一個執行了,下面就開始分析 錯誤、重定向攔截器。

RetryAndFollowUpInterceptor

在上面介紹攔截器的時候講過,它是錯誤重連、重定向的攔截器,下面咱們看它的核心代碼

public final class RetryAndFollowUpInterceptor implements Interceptor {
  
 
  @Override public Response intercept(Chain chain) throws IOException {
    //拿到當前的請求
    Request request = chain.request();
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    //拿到 Transmitter 對象
    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) {
        //檢查是否能夠繼續使用
        if (!recover(e.getLastConnectException(), transmitter, false, request)) 				{
          throw e.getFirstConnectException();
        }
        continue;
      } catch (IOException e) {
        boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
        //檢查是否能夠繼續使用
        if (!recover(e, transmitter, requestSendStarted, request)) throw e;
        continue;
      } finally {
        //若是未成功 釋放鏈接
        if (!success) {
          transmitter.exchangeDoneDueToException();
        }
      }

      //執行到這裏說明沒有出現異常
      if (priorResponse != null) {
        response = response.newBuilder()
            .priorResponse(priorResponse.newBuilder()
                    .body(null)
                    .build())
            .build();
      }
			...//省略代碼
      //根據響應來處理請求頭
      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();
      }
			
      //重定向的次數不能大於 20
      if (++followUpCount > MAX_FOLLOW_UPS) {
        throw new ProtocolException("Too many follow-up requests: " + followUpCount);
      }
			//根據重定向以後的請求再次重試
      request = followUp;
      priorResponse = response;
    }
  } 
}
複製代碼

根據上面代碼分析能夠知道,主要作了如下幾點

  1. 拿到當前的請求對象,並拿到 Transmitter 對象
  2. 準備鏈接,其實真正鏈接是在ConnectInterceptor 攔截器中
  3. 調用下一個攔截器,也就是 BridgeInterceptor 將請求交於它在預處理。
  4. 在鏈接的過程當中是否出現異常,判斷是否支持繼續鏈接
  5. 若是沒有成功就釋放資源
  6. 根據響應碼判斷是否須要重連操做
  7. 若是重連次數大於 20 次則拋異常,不然就將重定向以後的請求重試。

當前 RetryAndFollowUpInterceptor 中的 realChain.proceed(request, transmitter, null); 調用走到了 BridgeInterceptor 應用與網絡交互的攔截器。

BridgeInterceptor

當上一個攔截器調用了 proceed 函數以後就會走到當前 intercept 函數裏面,裏面具體操做咱們看下源碼處理

public final class BridgeInterceptor implements Interceptor {
  private final CookieJar cookieJar;

	...//省略構造函數

  @Override public Response intercept(Chain chain) throws IOException {
    //拿到當前請求 Request
    Request userRequest = chain.request();
    //拿到 Request 配置參數的 Builder
    Request.Builder requestBuilder = userRequest.newBuilder();
		//獲取到請求體 body
    RequestBody body = userRequest.body();
    //判斷請求體是否爲空
    if (body != null) {//不爲空的狀況下
      //獲取請求體類型
      MediaType contentType = body.contentType();
      if (contentType != null) {
        //將請求體類型添加 header
        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");
      }
    }
		
    //添加header HOST 主機
    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;
      //添加 gzip 壓縮
      requestBuilder.header("Accept-Encoding", "gzip");
    }

    List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
    if (!cookies.isEmpty()) {
     	//header 中添加 cookie
      requestBuilder.header("Cookie", cookieHeader(cookies));
    }
		//添加 user-agent
    if (userRequest.header("User-Agent") == null) {
      requestBuilder.header("User-Agent", Version.userAgent());
    }
		
    //執行下一個攔截器 CacheInterceptor
    Response networkResponse = chain.proceed(requestBuilder.build());
		//對 url 和 cookie 保存
    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 主要是對請求頭作一些預處理,以後就調用下一個攔截器。

CacheInterceptor

根據上一個攔截器 BridgeInterceptor 調用最後會走到當前的 intercept , 根據上面的攔截器介紹知道,它是獲取緩存和更新緩存的做用。下面咱們看下它具體實現

public final class CacheInterceptor implements Interceptor {
  final @Nullable InternalCache cache;

	...//構造函數省略

  @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;
    //拿到緩存響應
    Response cacheResponse = strategy.cacheResponse;
    
		...//省略部分代碼
    //若是請求跟緩存響應爲空的話,就強制使用緩存,返回錯誤碼爲 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();
    }

    // 若是 networkRequest 爲空的話,也強制獲取緩存
    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }

    
    Response networkResponse = null;
    try {
      //調用下一個攔截器
      networkResponse = chain.proceed(networkRequest);
    } finally {
     ...
    }

    // 若是緩存不爲空
    if (cacheResponse != null) {
      //而且響應碼 == 以前定義的 304
      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)) {
        // 存入緩存
        CacheRequest cacheRequest = cache.put(response);
        return cacheWritingResponse(cacheRequest, response);
      }
			
      //檢查緩存是否有效
      if (HttpMethod.invalidatesCache(networkRequest.method())) {
        try {
          //刪除無效緩存
          cache.remove(networkRequest);
        } catch (IOException ignored) {
          
        }
      }
    }
    return response;
  }
複製代碼

能夠看到這裏主要是對緩存作處理,因爲這裏只講攔截器的調用和一些基本處理邏輯,OKHttp 緩存機制後面會單獨用一篇文章來介紹,在這裏只要知道,若是外部 OKHttpClient 配置了緩存的話(看下面代碼塊,否則緩存都是空的,也不會默認添加緩存),纔會執行緩存 put、get、update,因爲這裏咱們沒有配置緩存策略,因此直接調用下一個攔截器,也就是 ConnectInterceptor

File file = new File(Environment.getExternalStorageDirectory() + "/T01");
Cache cache = new Cache(file, 1024 * 1024 * 10);
OkHttpClient okHttpClient = new OkHttpClient.Builder().
                        addInterceptor(new LoggingInterceptor())
                        .cache(cache).
                        build();
複製代碼

ConnectInterceptor

(ps: 攔截攔截器主要參考了:juejin.im/post/5d8364…

緩存攔截器執行完成以後, 下一個調用鏈就是鏈接攔截器了,看一下代碼實現:

public final class ConnectInterceptor implements Interceptor {
  public final OkHttpClient client;

  public ConnectInterceptor(OkHttpClient client) {
    this.client = client;
  }

  @Override public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    //拿到請求
    Request request = realChain.request();
    //拿到 Transmitter 
    Transmitter transmitter = realChain.transmitter();
    
    boolean doExtensiveHealthChecks = !request.method().equals("GET");
    //從新建立一個 Exchange
    Exchange exchange = transmitter.newExchange(chain, doExtensiveHealthChecks);
		//調用proceed方法,裏面調用下一個攔截器CallServerInterceptor的intercept方法
    return realChain.proceed(request, transmitter, exchange);
  }
}
複製代碼

經過上面代碼能夠看出 ConnectInterceptor 內部代碼很簡潔,首先拿到 Request 請求,獲取 Transmitter 對象,其次是經過 transmitter 從新建立一個 Exchange , Exchange 是負責將數據寫入到建立鏈接的 IO 流中的交互動做,最後在調用 CallServerInterceptor 攔截器。咱們看下 transmitter.newExchange(chain, doExtensiveHealthChecks) 內部代碼實現

Exchange newExchange(Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
    synchronized (connectionPool) {
      //若是沒有 Exchanges 拋一個異常
      if (noMoreExchanges) {
        throw new IllegalStateException("released");
      }
      if (exchange != null) {
        ...//省略拋異常代碼
    }
		
    //經過ExchangeFinder的find方法找到一個ExchangeCodec
    ExchangeCodec codec = exchangeFinder.find(client, chain, doExtensiveHealthChecks);
    //建立Exchange,並把ExchangeCodec實例codec傳進去,因此Exchange內部持有ExchangeCodec實例
    Exchange result = new Exchange(this, call, eventListener, exchangeFinder, codec);

    synchronized (connectionPool) {
      this.exchange = result;
      this.exchangeRequestDone = false;
      this.exchangeResponseDone = false;
      return result;
    }
  }
複製代碼

ExchangeFinder 對象早在RetryAndFollowUpInterceptor中經過TransmitterprepareToConnect方法建立,它的 find 方法是鏈接真正建立的地方,ExchangeFinder 是什麼?ExchangeFinder 就是負責鏈接的建立,把建立好的鏈接放入鏈接池,若是鏈接池中已經有該鏈接,就直接取出複用,因此 ExchangeFinder 管理着兩個重要的角色:RealConnectionRealConnectionPool,下面講解一下 RealConnectionPoolRealConnection,有助於鏈接機制的理解。

RealConnection

鏈接的真正實現,實現了 Connection 接口,內部利用 Socket 創建鏈接,以下:

public interface Connection {
    //返回這個鏈接使用的Route
    Route route();

    //返回這個鏈接使用的Socket
    Socket socket();

    //若是是HTTPS,返回TLS握手信息用於創建鏈接,不然返回null
    @Nullable Handshake handshake();

    //返回應用層使用的協議,Protocol是一個枚舉,如HTTP1.一、HTTP2
    Protocol protocol();
}

public final class RealConnection extends Http2Connection.Listener implements Connection {

    public final RealConnectionPool connectionPool;
    //路由
    private final Route route;
    //內部使用這個rawSocket在TCP層創建鏈接
    private Socket rawSocket;
    //若是沒有使用HTTPS,那麼socket == rawSocket,不然這個socket == SSLSocket
    private Socket socket;
    //TLS握手
    private Handshake handshake;
    //應用層協議
    private Protocol protocol;
    //HTTP2鏈接
    private Http2Connection http2Connection;
    //okio庫的BufferedSource和BufferedSink,至關於javaIO的輸入輸出流
    private BufferedSource source;
    private BufferedSink sink;


    public RealConnection(RealConnectionPool connectionPool, Route route) {
        this.connectionPool = connectionPool;
        this.route = route;
    }


    public void connect(int connectTimeout, int readTimeout, int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled, Call call, EventListener eventListener) {
        //...
    }

    //...
}

複製代碼

RealConnection 中有一個 connect 方法,外部能夠調用該方法創建鏈接,connect 方法以下:

//RealConnection.java
public void connect(int connectTimeout, int readTimeout, int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled, Call call, EventListener eventListener) {
    if (protocol != null) throw new IllegalStateException("already connected");

    RouteException routeException = null;
    List<ConnectionSpec> connectionSpecs = route.address().connectionSpecs();
    ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);

    //路由選擇
    if (route.address().sslSocketFactory() == null) {
      if (!connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) {
        throw new RouteException(new UnknownServiceException(
            "CLEARTEXT communication not enabled for client"));
      }
      String host = route.address().url().host();
      if (!Platform.get().isCleartextTrafficPermitted(host)) {
        throw new RouteException(new UnknownServiceException(
            "CLEARTEXT communication to " + host + " not permitted by network security policy"));
      }
    } else {
      if (route.address().protocols().contains(Protocol.H2_PRIOR_KNOWLEDGE)) {
        throw new RouteException(new UnknownServiceException(
            "H2_PRIOR_KNOWLEDGE cannot be used with HTTPS"));
      }
    }

    //開始鏈接
    while (true) {
      try {
        if (route.requiresTunnel()) {//若是是通道模式,則創建通道鏈接
          connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener);
          if (rawSocket == null) {
            // We were unable to connect the tunnel but properly closed down our resources.
            break;
          }
        } else {//一、不然進行Socket鏈接,大部分是這種狀況
          connectSocket(connectTimeout, readTimeout, call, eventListener);
        }
        //創建HTTPS鏈接
        establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener);
        break;
      }
      //...省略異常處理

    if (http2Connection != null) {
      synchronized (connectionPool) {
        allocationLimit = http2Connection.maxConcurrentStreams();
      }
    }
  }

複製代碼

咱們關注註釋1,通常會調用 connectSocket 方法創建 Socket 鏈接,connectSocket 方法以下:

//RealConnection.java
private void connectSocket(int connectTimeout, int readTimeout, Call call, EventListener eventListener) throws IOException {
    Proxy proxy = route.proxy();
    Address address = route.address();

    //根據代理類型的不一樣建立Socket
    rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
        ? address.socketFactory().createSocket()
        : new Socket(proxy);

    eventListener.connectStart(call, route.socketAddress(), proxy);
    rawSocket.setSoTimeout(readTimeout);
    try {
        //一、創建Socket鏈接
        Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
    }
    //...省略異常處理

    try {
        //得到Socket的輸入輸出流
        source = Okio.buffer(Okio.source(rawSocket));
        sink = Okio.buffer(Okio.sink(rawSocket));
    } 
     //...省略異常處理
}

複製代碼

咱們關注註釋1,Platform 是 okhttp 中根據不一樣 Android 版本平臺的差別實現的一個兼容類,這裏就不細究,Platform 的 connectSocket 方法最終會調用 rawSocket 的 connect() 方法創建其Socket 鏈接,創建 Socket 鏈接後,就能夠經過 Socket 鏈接得到輸入輸出流 source 和 sink,okhttp 就能夠從 source 讀取或往 sink 寫入數據,source 和 sink 是 BufferedSource 和BufferedSink 類型,它們是來自於okio庫,它是一個封裝了 java.io 和 java.nio 的庫,okhttp 底層依賴這個庫讀寫數據,想要了解 okio 這個庫能夠看這篇文章拆輪子系列:拆 Okio

RealConnectionPool

鏈接池,用來管理鏈接對象 RealConnection ,以下:

public final class RealConnectionPool {

    //線程池
    private static final Executor executor = new ThreadPoolExecutor(
        0 /* corePoolSize */,
        Integer.MAX_VALUE /* maximumPoolSize */, 
        60L /* keepAliveTime */, 
        TimeUnit.SECONDS,
        new SynchronousQueue<>(), 
        Util.threadFactory("OkHttp ConnectionPool", true));
 
    boolean cleanupRunning;
    //清理鏈接任務,在executor中執行
    private final Runnable cleanupRunnable = () -> {
        while (true) {
            //調用cleanup方法執行清理邏輯
            long waitNanos = cleanup(System.nanoTime());
            if (waitNanos == -1) return;
            if (waitNanos > 0) {
                long waitMillis = waitNanos / 1000000L;
                waitNanos -= (waitMillis * 1000000L);
                synchronized (RealConnectionPool.this) {
                    try {
                        //調用wait方法進入等待
                        RealConnectionPool.this.wait(waitMillis, (int) waitNanos);
                    } catch (InterruptedException ignored) {
                    }
                }
            }
        }
    };

    //雙端隊列,保存鏈接
    private final Deque<RealConnection> connections = new ArrayDeque<>();

    void put(RealConnection connection) {
        if (!cleanupRunning) {
            cleanupRunning = true;
            //使用線程池執行清理任務
            executor.execute(cleanupRunnable);
        }
        //將新建鏈接插入隊列
        connections.add(connection);
    }

    long cleanup(long now) {
        //...
    }

    //...
}

複製代碼

RealConnectionPool 在內部維護了一個線程池,用來執行清理鏈接任務 cleanupRunnable ,還維護了一個雙端隊列 connections ,用來緩存已經建立的鏈接。要知道建立一次鏈接要經歷 TCP握手,若是是 HTTPS 還要經歷 TLS 握手,握手的過程都是耗時的,因此爲了提升效率,就須要connections 來對鏈接進行緩存,從而能夠複用;還有若是鏈接使用完畢,長時間不釋放,也會形成資源的浪費,因此就須要 cleanupRunnable 定時清理無用的鏈接,okhttp 支持 5 個併發鏈接,默認每一個鏈接 keepAlive 爲 5 分鐘,keepAlive 就是鏈接空閒後,保持存活的時間。

當咱們第一次調用 RealConnectionPool 的 put 方法緩存新建鏈接時,若是 cleanupRunnable 還沒執行,它首先會使用線程池執行 cleanupRunnable ,而後把新建鏈接放入雙端隊列,cleanupRunnable 中會調用 cleanup 方法進行鏈接的清理,該方法返回如今到下次清理的時間間隔,而後調用 wiat 方法進入等待狀態,等時間到了後,再次調用 cleanup 方法進行清理,就這樣往復循環。咱們來看一下 cleanup 方法的清理邏輯:

//RealConnectionPool.java
long cleanup(long now) {
    
    int inUseConnectionCount = 0;//正在使用鏈接數
    int idleConnectionCount = 0;//空閒鏈接數
    RealConnection longestIdleConnection = null;
    long longestIdleDurationNs = Long.MIN_VALUE;

    synchronized (this) {
        //遍歷全部鏈接,記錄空閒鏈接和正在使用鏈接各自的數量
        for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
            RealConnection connection = i.next();

            //若是該鏈接還在使用,pruneAndGetAllocationCount種經過引用計數的方式判斷一個鏈接是否空閒
            if (pruneAndGetAllocationCount(connection, now) > 0) {
                //使用鏈接數加1
                inUseConnectionCount++;
                continue;
            }
            
            //該鏈接沒有在使用

            //空閒鏈接數加1
            idleConnectionCount++;

            //記錄keepalive時間最長的那個空閒鏈接
            long idleDurationNs = now - connection.idleAtNanos;
            if (idleDurationNs > longestIdleDurationNs) {
                longestIdleDurationNs = idleDurationNs;
                //這個鏈接極可能被移除,由於空閒時間太長
                longestIdleConnection = connection;
            }
        }
        
        //跳出循環後

        //默認keepalive時間keepAliveDurationNs最長爲5分鐘,空閒鏈接數idleConnectionCount最大爲5個
        if (longestIdleDurationNs >= this.keepAliveDurationNs || idleConnectionCount > this.maxIdleConnections) {//若是longestIdleConnection的keepalive時間大於5分鐘 或 空閒鏈接數超過5個
            //把longestIdleConnection鏈接從隊列清理掉
            connections.remove(longestIdleConnection);
        } else if (idleConnectionCount > 0) {//若是空閒鏈接數小於5個 而且 longestIdleConnection鏈接還沒到期清理
            //返回該鏈接的到期時間,下次再清理
            return keepAliveDurationNs - longestIdleDurationNs;
        } else if (inUseConnectionCount > 0) {//若是沒有空閒鏈接 且 全部鏈接都還在使用
            //返回keepAliveDurationNs,5分鐘後再清理
            return keepAliveDurationNs;
        } else {
            // 沒有任何鏈接,把cleanupRunning復位
            cleanupRunning = false;
            return -1;
        }
    }

    //把longestIdleConnection鏈接從隊列清理掉後,關閉該鏈接的socket,返回0,當即再次進行清理
    closeQuietly(longestIdleConnection.socket());

    return 0;
}

複製代碼

從 cleanup 方法得知,okhttp 清理鏈接的邏輯以下:

一、首先遍歷全部鏈接,記錄空閒鏈接數 idleConnectionCount 和正在使用鏈接數inUseConnectionCount ,在記錄空閒鏈接數時,還要找出空閒時間最長的空閒鏈接longestIdleConnection,這個鏈接是頗有可能被清理的;

二、遍歷完後,根據最大空閒時長和最大空閒鏈接數來決定是否清理longestIdleConnection,

2.一、若是 longestIdleConnection 的空閒時間大於最大空閒時長 或 空閒鏈接數大於最大空閒鏈接數,那麼該鏈接就會被從隊列中移除,而後關閉該鏈接的 socket,返回 0,當即再次進行清理;

2.二、若是空閒鏈接數小於5個 而且 longestIdleConnection 的空閒時間小於最大空閒時長即還沒到期清理,那麼返回該鏈接的到期時間,下次再清理;

2.三、若是沒有空閒鏈接 且 全部鏈接都還在使用,那麼返回默認的 keepAlive 時間,5分鐘後再清理;

2.四、沒有任何鏈接,idleConnectionCount 和 inUseConnectionCount 都爲0,把cleanupRunning 復位,等待下一次 put 鏈接時,再次使用線程池執行 cleanupRunnable。

瞭解了 RealConnectionPool 和 RealConnection 後,咱們再回到 ExchangeFinder 的 find 方法,這裏是鏈接建立的地方。

鏈接機制

ExchangeFinder的fing方法以下:

//ExchangeFinder.java

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

    try {
      //1.內部調用 findHealthyConnection 函數返回 RealConnection 鏈接對象
      RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
          writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
      //2. 創建一個新的鏈接
      return resultConnection.newCodec(client, chain);
    } catch (RouteException e) {
      ...//省略異常處理
    }
  }
複製代碼

根據註釋 1 咱們知道建立一個 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;
        }
      }

      //主要判斷鏈接的可用性
      if (!candidate.isHealthy(doExtensiveHealthChecks)) {
        candidate.noNewExchanges();
        continue;
      }

      return candidate;
    }
  }
複製代碼

接着看 findConnection

//ExchangeFinder.java
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; .

	 //一、嘗試使用已經建立過的鏈接,已經建立過的鏈接可能已經被限制建立新的流
      releasedConnection = transmitter.connection;
      //1.一、若是已經建立過的鏈接已經被限制建立新的流,就釋放該鏈接(releaseConnectionNoEvents中會把該鏈接置空),並返回該鏈接的Socket以關閉
      toClose = transmitter.connection != null && transmitter.connection.noNewExchanges
          ? transmitter.releaseConnectionNoEvents()
          : null;

        //1.二、已經建立過的鏈接還能使用,就直接使用它看成結果、
        if (transmitter.connection != null) {
            result = transmitter.connection;
            releasedConnection = null;
        }

        //二、已經建立過的鏈接不能使用
        if (result == null) {
            //2.一、嘗試從鏈接池中找可用的鏈接,若是找到,這個鏈接會賦值先保存在Transmitter中
            if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, null, false)) {
                //2.二、從鏈接池中找到可用的鏈接
                foundPooledConnection = true;
                result = transmitter.connection;
            } else if (nextRouteToTry != null) {
                selectedRoute = nextRouteToTry;
                nextRouteToTry = null;
            } else if (retryCurrentRoute()) {
                selectedRoute = transmitter.connection.route();
            }
        }
    }
	closeQuietly(toClose);
    
	//...
    
    if (result != null) {
        //三、若是在上面已經找到了可用鏈接,直接返回結果
        return result;
    }
    
    //走到這裏沒有找到可用鏈接

    //看看是否須要路由選擇,多IP操做
    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)) {
                //4.一、從鏈接池中找到可用的鏈接
                foundPooledConnection = true;
                result = transmitter.connection;
            }
        }

        //在鏈接池中沒有找到可用鏈接
        if (!foundPooledConnection) {
            if (selectedRoute == null) {
                selectedRoute = routeSelection.next();
            }

           //五、因此這裏新建立一個鏈接,後面會進行Socket鏈接
            result = new RealConnection(connectionPool, selectedRoute);
            connectingConnection = result;
        }
    }

    // 4.二、若是在鏈接池中找到可用的鏈接,直接返回該鏈接
    if (foundPooledConnection) {
        eventListener.connectionAcquired(call, result);
        return result;
    }

    //5.一、調用RealConnection的connect方法進行Socket鏈接,這個在RealConnection中講過
    result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis, connectionRetryEnabled, call, eventListener);
    
    connectionPool.routeDatabase.connected(result.route());

    Socket socket = null;
    synchronized (connectionPool) {
        connectingConnection = null;
        //若是咱們剛剛建立了同一地址的多路複用鏈接,釋放這個鏈接並獲取那個鏈接
        if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, true)) {
            result.noNewExchanges = true;
            socket = result.socket();
            result = transmitter.connection;
        } else {
            //5.二、把剛剛新建的鏈接放入鏈接池
            connectionPool.put(result);
            //5.三、把剛剛新建的鏈接保存到Transmitter的connection字段
            transmitter.acquireConnectionNoEvents(result);
        }
    }
    
    closeQuietly(socket);
    eventListener.connectionAcquired(call, result);
    
    //5.四、返回結果
    return result;
}

複製代碼

這個findConnection方法就是整個ConnectInterceptor的核心,咱們忽略掉多IP操做和多路複用(HTTP2),假設如今咱們是第一次請求,鏈接池和Transmitter中沒有該鏈接,因此跳過一、二、3,直接來到5,建立一個新的鏈接,而後把它放入鏈接池和Transmitter中;接着咱們用同一個Call進行了第二次請求,這時鏈接池和Transmitter中有該鏈接,因此就會走一、二、3,若是Transmitter中的鏈接還可用就返回,不然從鏈接池獲取一個可用鏈接返回,因此整個鏈接機制的大概過程以下:

Transmitter 中的鏈接和鏈接池中的鏈接有什麼區別?咱們知道每建立一個 Call,就會建立一個對應的 Transmitter ,一個 Call 能夠發起屢次請求(同步、異步),不一樣的 Call 有不一樣的Transmitter ,鏈接池是在建立 OkhttpClient 時建立的,因此鏈接池是全部 Call 共享的,即鏈接池中的鏈接全部 Call 均可以複用,而 Transmitter 中的那個鏈接只是對應它相應的 Call,只能被本次 Call 的全部請求複用。

瞭解了 okhttp3 的鏈接機制後,咱們接着下一個攔截器 networkInterceptors 。

networkInterceptors

networkInterceptors 它是 OKHttp 攔截器中的第 6 個攔截器,屬於 網絡攔截器,那麼它的做用是什麼請看下面 攔截器實戰 中介紹。

最後執行到了 OKHttp 最後一個攔截器 CallServerInterceptor

CallServerInterceptor

根據源碼中的介紹:它是鏈中的最後一個攔截器。它與服務器進行網絡請求、響應操做。

@Override public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    //拿到 Exchange 與 網絡交互
    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) {
      //若是請求頭添加了 100-continue 
      if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
        exchange.flushRequest(); //關閉 IO 流資源
        responseHeadersStarted = true;
        exchange.responseHeadersStart();
        responseBuilder = exchange.readResponseHeaders(true); 
      }

      if (responseBuilder == null) { //若是爲空
        if (request.body().isDuplex()) {
          exchange.flushRequest();
          BufferedSink bufferedRequestBody = Okio.buffer(
              exchange.createRequestBody(request, true));
          request.body().writeTo(bufferedRequestBody);
        } else { //通常走 else
          //寫入請求體的操做
          BufferedSink bufferedRequestBody = Okio.buffer(
              exchange.createRequestBody(request, false));
          request.body().writeTo(bufferedRequestBody);
          bufferedRequestBody.close();
        }
      } else {
        exchange.noRequestBody();
        if (!exchange.connection().isMultiplexed()) {
          // 
          exchange.noNewExchangesOnConnection();
        }
      }
    } else { //若是沒有請求體 執行 noRequestBody
      exchange.noRequestBody();
    }

    //若是請求體爲空 而且不支持 isDuplex = false IO 流
    if (request.body() == null || !request.body().isDuplex()) {
      exchange.finishRequest();
    }

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

    //讀取響應的 head
    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) {
      // 構建響應
      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) {
      // 構建空響應體
      response = response.newBuilder()
          .body(Util.EMPTY_RESPONSE)
          .build();
    } else {
      // 經過響應的 body 構造 響應體
      response = response.newBuilder()
          .body(exchange.openResponseBody(response))
          .build();
    }

   ...//省略部分代碼

    return response;
  }
複製代碼

在當前攔截器中咱們把請求 head /body 經過 okio 寫入了服務端,而後根據服務端的響應數據構建響應頭、響應體等一些響應數據。

到這裏咱們完成了攔截器全部操做,下面進入攔截器實戰。

攔截器實戰

OKHttp官網攔截器使用介紹

自定義 Log 打印攔截器

/** * 打印日誌攔截器 */
class LoggingInterceptor implements Interceptor {
    private String TAG = "LoggingInterceptor";
    public static String requestBodyToString(RequestBody requestBody) throws IOException {
        if (requestBody == null)return "";
        Buffer buffer = new Buffer();
        requestBody.writeTo(buffer);
        return buffer.readUtf8();
    }

    @Override
    public Response intercept(Chain chain) throws IOException {
        //拿到請求數據
        Request request = chain.request();


        //能夠在請求服務器以前添加請求頭
        request =   request.newBuilder()
                .addHeader("head-1","1")
                .addHeader("head-2","2")
                .url("https://juejin.im/user/578259398ac2470061f3a3fb")
                .build();


        HttpUrl url = request.url();
        String scheme = url.scheme();// http https
        String host = url.host();// 127.0.0.1
        String path = url.encodedPath();// /test/upload/img
        String query = url.encodedQuery();// userName=DevYk&userPassword=12345
        RequestBody requestBody = request.body();
        String bodyToString = requestBodyToString(requestBody);

        Log.d(TAG,"scheme--》"+scheme);
        Log.d(TAG,"Host--->"+host);
        Log.d(TAG,"path--->"+path);
        Log.d(TAG,"query--->"+query);
        Log.d(TAG,"requestBody---->"+bodyToString+"");
        Log.d(TAG,"head---->"+request.headers().names());

        //調用下一個攔截器
        Response response = chain.proceed(request);
        
       //拿到響應
        ResponseBody responseBody = response.body();
        String body = responseBody.string();
        String type = responseBody.contentType().type();
        String subtype = responseBody.contentType().subtype();

        //打印響應
        Log.d(TAG,"contentType--->"+type+" "+subtype);
        Log.d(TAG,"responseBody--->"+body);

        return chain.proceed(request);
    }
}
複製代碼

添加配置

OkHttpClient okHttpClient = new OkHttpClient.Builder().
        addInterceptor(new LoggingInterceptor())
        build();
複製代碼

output:

LoggingInterceptor: scheme--》https
LoggingInterceptor: Host--->juejin.im
LoggingInterceptor: path--->/user/578259398ac2470061f3a3fb
LoggingInterceptor: query--->null
  
LoggingInterceptor: requestBody---->
  
LoggingInterceptor: head---->[head-1, head-2]
LoggingInterceptor: responseHeader--->text html
LoggingInterceptor: responseBody---><!DOCTYPE html><html ....
  

複製代碼

自定義 全局禁止網絡請求攔截器

public class NetworkInterceptor implements Interceptor {
    @Override
    public okhttp3.Response intercept(Chain chain) throws IOException {
        if (true) {
            Response response = new Response.Builder()
                    .code(404) // 其實code能夠隨便給
                    .protocol(Protocol.HTTP_1_1)
                    .message("根據規定,暫時不能進行網絡請求。")
                    .body(ResponseBody.create(MediaType.get("text/html; charset=utf-8"), "")) // 返回空頁面
                    .request(chain.request())
                    .build();
            return response;
        } else {
            return chain.proceed(chain.request());
        }
    }
}
複製代碼

配置

OkHttpClient okHttpClient = new OkHttpClient.Builder().
        addInterceptor(new LoggingInterceptor()).
  			addInterceptor(new NetworkInterceptor()).
        build();
複製代碼

Output:

LoggingInterceptor: responseCode--->404
LoggingInterceptor: responseMessage--->根據規定,暫時不能進行網絡請求。
LoggingInterceptor: responseisSuccessful--->false
複製代碼

小總結:攔截器分爲 應用攔截器、網絡攔截器 根據官網解釋有這幾點:

應用攔截器

  • 無需擔憂中間響應,例如重定向和重試。
  • 即便從緩存提供 HTTP 響應,也老是被調用一次。
  • 遵照應用程序的原始意圖。不關心 OkHttp 注入的標頭,例如 If-None-Match
  • 容許短路而不是Chain.proceed()
  • 容許重試並屢次致電Chain.proceed()

網絡攔截器

  • 可以對諸如重定向和重試之類的中間響應進行操做。
  • 不會爲使網絡短路的緩存響應調用。
  • 觀察數據,就像經過網絡傳輸數據同樣。
  • 訪問Connection帶有請求的。

因此怎麼選擇看本身需求了。

攔截器總結

根據上面的攔截器講解和實戰,相信你們對 OKHttp 攔截器有了必定的認識,這裏咱們根據分析來總結下:

其實每個攔截器都對應一個 RealInterceptorChain ,而後每個interceptor 再產生下一個RealInterceptorChain,直到 List 迭代完成。因此上面基本上就是遞歸,找了一些圖片有助於你們理解以下圖

uxSrhd.png

參考

OKHttp源碼解析(四)--中階之攔截器及調用鏈

OKHttp源碼分析

相關文章
相關標籤/搜索