OkHttp必須弄清楚的幾個原理性問題

前言

okhttp是目前很火的網絡請求框架,Android4.4開始HttpURLConnection的底層就是採用okhttp實現的,其Github地址:github.com/square/okht…git

來自官方說明:github

OkHttp is an HTTP client that’s efficient by default:設計模式

  • HTTP/2 support allows all requests to the same host to share a socket.
  • Connection pooling reduces request latency (if HTTP/2 isn’t available).
  • Transparent GZIP shrinks download sizes.
  • Response caching avoids the network completely for repeat requests.

總結一下,OkHttp支持http2,固然須要你請求的服務端支持才行,針對http1.x,OkHttp採用了鏈接池下降網絡延遲,內部實現gzip透明傳輸,使用者無需關注,支持http協議上的緩存用於避免重複網絡請求。緩存

使用方法

  1. 引入依賴服務器

    implementation 'com.squareup.okhttp3:okhttp:3.14.4'
    複製代碼
  2. 請求網絡cookie

    OkHttpClient okHttpClient = new OkHttpClient();
     Request request = new Request.Builder().url("http://mtancode.com/").build();
     // 同步方式
     Response response = okHttpClient.newCall(request).execute();
     // 異步方式
     okHttpClient.newCall(request).enqueue(new Callback() {
         @Override
         public void onFailure(Call call, IOException e) {
             Log.i(TAG, "onFailure");
             e.printStackTrace();
         }
    
         @Override
         public void onResponse(Call call, Response response) {
             try {
                 Log.i(TAG, response.body().string());
             } catch (Throwable t) {
                 t.printStackTrace();
             }
         }
     });
    複製代碼

能夠看到,使用起來很是簡單,並且支持同步和異步兩種方式請求網絡。這裏須要注意一下,回調的線程並非UI線程。網絡

主流程分析

同步和異步只是使用方式不一樣,但其原理都是同樣的,最終會走到相同的邏輯,所以這裏就直接從異步方式開始分析了,newCall方法會返回一個RealCall對象,看其enqueue方法:框架

@Override public void enqueue(Callback responseCallback) {
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    transmitter.callStart();
    client.dispatcher().enqueue(new AsyncCall(responseCallback));
}
複製代碼

這裏有個Dispatcher,顧名思義它就是專門分發和執行請求的,看它的enqueue方法:異步

void enqueue(AsyncCall call) {
    synchronized (this) {
      readyAsyncCalls.add(call);

      // Mutate the AsyncCall so that it shares the AtomicInteger of an existing running call to
      // the same host.
      if (!call.get().forWebSocket) {
        AsyncCall existingCall = findExistingCallWithHost(call.host());
        if (existingCall != null) call.reuseCallsPerHostFrom(existingCall);
      }
    }
    promoteAndExecute();
  }
複製代碼

把call添加到readyAsyncCalls列表中,看promoteAndExecute方法:socket

private boolean promoteAndExecute() {
	assert (!Thread.holdsLock(this));

    List<AsyncCall> executableCalls = new ArrayList<>();
    boolean isRunning;
    synchronized (this) {
      for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
        AsyncCall asyncCall = i.next();

        if (runningAsyncCalls.size() >= maxRequests) break; // Max capacity.
        if (asyncCall.callsPerHost().get() >= maxRequestsPerHost) continue; // Host max capacity.

        i.remove();
        asyncCall.callsPerHost().incrementAndGet();
        executableCalls.add(asyncCall);
        runningAsyncCalls.add(asyncCall);
      }
      isRunning = runningCallsCount() > 0;
    }

    for (int i = 0, size = executableCalls.size(); i < size; i++) {
      AsyncCall asyncCall = executableCalls.get(i);
      asyncCall.executeOn(executorService());
    }

    return isRunning;
}
複製代碼

把call搬到runningAsyncCalls中,遍歷列表,對每一個call調用executeOn方法:

void executeOn(ExecutorService executorService) {
  assert (!Thread.holdsLock(client.dispatcher()));
  boolean success = false;
  try {
    executorService.execute(this);
    success = true;
  } catch (RejectedExecutionException e) {
    InterruptedIOException ioException = new InterruptedIOException("executor rejected");
    ioException.initCause(e);
    transmitter.noMoreExchanges(ioException);
    responseCallback.onFailure(RealCall.this, ioException);
  } finally {
    if (!success) {
      client.dispatcher().finished(this); // This call is no longer running!
    }
  }
}
複製代碼

看RealCall的execute方法:

@Override protected void execute() {
  boolean signalledCallback = false;
  transmitter.timeoutEnter();
  try {
    Response response = getResponseWithInterceptorChain();
    responseCallback.onResponse(RealCall.this, response);
......
}
複製代碼

來到getResponseWithInterceptorChain方法,該方法內部會執行全部具體的處理邏輯,執行結束後,返回一個最終的response,而後回調給外部傳入的callback,看看getResponseWithInterceptorChain方法:

Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());
    interceptors.add(new RetryAndFollowUpInterceptor(client));
    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, 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);
      }
    }
  }
複製代碼

能夠看到,這裏添加了一系列的攔截器,構成攔截器鏈,請求會沿着這條鏈依次調用其intercept方法,每一個攔截器都作本身該作的工做,最終完成請求,返回最終的response對象。

簡單說下鏈式調用的實現方法:建立一個RealInterceptorChain,傳入全部的interceptors,和當前index(從0開始),而後調用RealInterceptorChain的process方法,該方法裏,獲取到對應的interceptor,而後調用intercept方法,而在intercept方法中,會執行具體的處理邏輯,而後建立一個RealInterceptorChain,傳入全部的interceptors,和當前index+1,繼續調用RealInterceptorChain的process方法,如此重複直到index超過interceptors個數爲止。其實這種實現方式跟Task實現鏈式調用很相似,整個調用過程會建立一系列的中間對象。

繼續回到okhttp,這裏實際上是一種責任鏈設計模式,它的優勢有:

  • 能夠下降邏輯的耦合,相互獨立的邏輯寫到本身的攔截器中,也無需關注其它攔截器所作的事情。

  • 擴展性強,能夠添加新的攔截器。

固然它也有缺點:

  • 由於調用鏈路長,並且存在嵌套,遇到問題排查其它比較麻煩。

對於OkHttp,咱們能夠添加本身的攔截器:

OkHttpClient.Builder builder = new OkHttpClient().newBuilder();
builder.addInterceptor(new Interceptor() {
    @Override
    public Response intercept(Chain chain) throws IOException {
        // TODO 自定義邏輯
        
        return chain.proceed(chain.request());
    }
});
複製代碼

來到這裏,OkHttp的主流程就分析完了,至於具體的緩存邏輯,鏈接池邏輯,網絡請求這些,都是在對應的攔截器裏面實現的,下面對這些攔截器逐個進行分析。

緩存機制

代碼在CacheInterceptor類中,實現HTTP協議的緩存機制,OkHttp默認並不沒有開啓緩存,要本身傳入一個Cache對象。

先了解下HTTP協議的緩存機制:

首先緩存分爲三種:過時時間緩存、第一差別緩存和第二差別緩存,並且在優先級上,過時時間緩存 > 第一差別緩存 > 第二差別緩存。

過時時間緩存,就是經過HTTP響應頭部的字段控制:

  • expires:響應字段,絕對過時時間,HTTP1.0。

  • Cache-Control:響應字段,相對過時時間,HTTP1.1。注意若是值爲no-cache,表示跳過過時時間緩存邏輯,值爲no-store表示跳過過時時間緩存邏輯和差別緩存邏輯,也就是不使用緩存數據。

當客戶端請求時,發現緩存未過時,就直接返回緩存數據了,不請求網絡,不然,執行第一差別緩存邏輯:

  • If-None-Match:請求字段,值爲ETag。

  • ETag:響應字段,服務端會根據內容生成惟一的字符串。

若是服務端發現If-None-Match的值和當前ETag同樣,就說明數據內容沒有變化,就返回304,不然,執行第二差別緩存邏輯:

  • If-Modified-Since:請求字段,客戶端告訴服務端本地緩存的資源的上次修改時間。

  • Last-Modified:響應字段,服務端告訴客戶端資源的最後修改時間。

若是服務端發現If-Modified-Since的值就是資源的最後修改時間,就說明數據內容沒有變化,就返回304,不然,返回全部資源數據給客戶端,響應碼爲200。

回到OkHttp,CacheInterceptor攔截器處理的邏輯,其實就是上面所說的HTTP緩存邏輯,注意到OkHttp提供了一個現成的緩存類Cache,它採用DiskLruCache實現緩存策略,至於緩存的位置和大小,須要你本身指定。

這裏其實會有個問題,上面的緩存都是依賴HTTP協議自己的緩存機制的,若是咱們請求的服務器不支持這套緩存機制,或者須要實現更靈活的緩存管理,直接使用上面這套緩存機制就可能不太可行了,這時咱們能夠本身新增攔截器,自行實現緩存的管理。

鏈接池

鏈接池的邏輯在ConnectInterceptor攔截器中處理,看intercept方法:

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

    // We need the network to satisfy this request. Possibly for validating a conditional GET.
    boolean doExtensiveHealthChecks = !request.method().equals("GET");
    Exchange exchange = transmitter.newExchange(chain, doExtensiveHealthChecks);

    return realChain.proceed(request, transmitter, exchange);
}
複製代碼

關鍵代碼就是調用了Transmitter的newExchange方法,最終會獲得一個Exchange對象,該對象表示一條鏈接,用於後面實現請求和讀取響應數據,爲了不陷入代碼中沒法自拔,這裏就不一步一步跟蹤newExchange方法了,它最後會調用ExchangeFinder的findConnection的方法,這個方法就是在鏈接池中尋找可複用的鏈接,固然若是沒找到,就建立一個新的鏈接,OkHttp對鏈接池的管理是在RealConnectionPool類中:

public final class RealConnectionPool {
  /**
   * Background threads are used to cleanup expired connections. There will be at most a single
   * thread running per connection pool. The thread pool executor permits the pool itself to be
   * garbage collected.
   */
  private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */,
      Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS,
      new SynchronousQueue<>(), Util.threadFactory("OkHttp ConnectionPool", true));

  /** The maximum number of idle connections for each address. */
  private final int maxIdleConnections;
  private final long keepAliveDurationNs;
  private final Runnable cleanupRunnable = () -> {
    while (true) {
      long waitNanos = cleanup(System.nanoTime());
      if (waitNanos == -1) return;
      if (waitNanos > 0) {
        long waitMillis = waitNanos / 1000000L;
        waitNanos -= (waitMillis * 1000000L);
        synchronized (RealConnectionPool.this) {
          try {
            RealConnectionPool.this.wait(waitMillis, (int) waitNanos);
          } catch (InterruptedException ignored) {
          }
        }
      }
    }
  };

  private final Deque<RealConnection> connections = new ArrayDeque<>();
  ......
}
複製代碼

主要關注幾個重要的成員變量,maxIdleConnections表示鏈接池的最大緩存鏈接數,這裏外部傳入了5,也就是最多緩存5個鏈接,緩存的鏈接都被放到connections中,而keepAliveDurationNs表示鏈接的緩存時長,這裏爲5分鐘,咱們還看到這裏還有個executor,它就是用來清理過時鏈接。

數據傳輸

在CallServerInterceptor攔截器中處理,採用okio實現,http請求和讀取響應最終是在Http1ExchangeCodec或Http2ExchangeCodec中實現的。

透明gzip壓縮

在BridgeInterceptor攔截器中處理,看一下intercept方法:

@Override public Response intercept(Chain chain) throws IOException {
    Request userRequest = chain.request();
    Request.Builder requestBuilder = userRequest.newBuilder();

    RequestBody body = userRequest.body();
	......

    // If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing
    // the transfer stream.
    boolean transparentGzip = false;
    if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
      transparentGzip = true;
      requestBuilder.header("Accept-Encoding", "gzip");
    }

    ......

    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();
  }
複製代碼

能夠看到,OkHttp默認會爲咱們加上gzip頭部字段,若是服務端支持的話,就會返回gzip壓縮後的數據,這樣就能夠縮短傳輸時間和減小傳輸數據大小,接收到gzip壓縮後的數據後,Okhttp會自動幫咱們解壓縮,因此這一切對使用者來講都是透明的,無需關注,固然若是咱們本身明確指定了用gzip壓縮,解壓縮的事情就須要咱們本身來作了。

支持HTTP2

OkHttp2支持HTTP2協議,固然若是服務端不支持就沒辦法了,針對HTTP2的相關類都在okhttp3.internal.http2包下,有興趣能夠自行查看源碼。

關於HTTP2的優勢,主要有:

  • 多路複用:就是針對同個域名的請求,均可以在同一條鏈接中並行進行,並且頭部和數據都進行了二進制封裝。

  • 二進制分幀:傳輸都是基於字節流進行的,而不是文本,二進制分幀層處於應用層和傳輸層之間。

  • 頭部壓縮:HTTP1.x每次請求都會攜帶完整的頭部字段,因此可能會出現重複傳輸,所以HTTP2採用HPACK對其進行壓縮優化,能夠節省很多的傳輸流量。

  • 服務端推送:服務端能夠主動推送數據給客戶端。

參考文章

解密HTTP/2與HTTP/3 的新特性

相關文章
相關標籤/搜索