OKHTTP3源碼和設計模式(上篇)

本文來探究一下 OkHttp3 的源碼和其中的設計思想。

關於 OkHttp3 的源碼分析的文章挺多,不過大多還是在為了源碼而源碼。個人覺得如果讀源碼不去分析源碼背後的設計模式或設計思想,那麼讀源碼的意義不大。 同時,如果熟悉的設計模式越多,那麼讀某個框架的源碼的時候就越容易,兩者是相輔相成的,這也是許多大牛認為多讀源碼能提高編程能力的原因。

整體架構

OKHTTP3源碼和設計模式(上篇)

為了方面後面的理解,我這裡簡單畫了個架構圖,圖中畫出了 OkHttp3 核心的功能模塊。為了方便整體理解,這裡分了三個層次: 客戶層、執行層和連接層。

首先,客戶層的OkHttpClient ,使用過 OkHttp 網絡庫的同學應該都熟悉,在發送網絡請求,執行層決定怎麼處理請求,比如同步還是異步,同步請求的話直接在當前線程完成請求, 請求要經過多層攔截器處理; 如果是異步處理,需要 Dispatcher 執行分發策略, 線程池管理執行任務; 又比如,一個請求下來,要不要走緩存,如果不走緩存,進行網絡請求。最後執行層將從連接層進行網絡 IO 獲取數據。

OkHttpClient

使用過 OkHttp 網絡庫的同學應該都熟悉 OkHttpClient , 許多第三方框架都會提供一個類似的類作為客戶訪問的一個入口。 關於 OkHttpClient 代碼註釋上就說的很清楚:


  1. /**
  2. * Factory for {@linkplain Call calls}, which can be used to send
  3. HTTP requests and read their
  4. * responses.
  5. *
  6. *

    OkHttpClients should be shared

  7. *
  8. *

    OkHttp performs best when you create a single {@code

  9. OkHttpClient} instance and reuse it for
  10. * all of your HTTP calls. This is because each client holds its own
  11. connection pool and thread
  12. * pools. Reusing connections and threads reduces latency and
  13. saves memory. Conversely, creating a
  14. * client for each request wastes resources on idle pools.
  15. *
  16. *

    Use {@code new OkHttpClient()} to create a shared instance

  17. with the default settings:
  18. *
     {@code
  19. *
  20. * // The singleton HTTP client.
  21. * public final OkHttpClient client = new OkHttpClient();
  22. * }
  23. *
  24. *

    Or use {@code new OkHttpClient.Builder()} to create a shared

  25. instance with custom settings:
  26. *
     {@code
  27. *
  28. * // The singleton HTTP client.
  29. * public final OkHttpClient client = new OkHttpClient.Builder()
  30. * .addInterceptor(new HttpLoggingInterceptor())
  31. * .cache(new Cache(cacheDir, cacheSize))
  32. * .build();
  33. * }
  34. *
  35. .... 省略
  36. */

簡單提煉:

1、OkHttpClient, 可以通過 new OkHttpClient() 或 new OkHttpClient.Builder() 來創建對象, 但是—特別注意, OkHttpClient() 對象最好是共享的, 建議使用單例模式創建。 因為每個 OkHttpClient 對象都管理自己獨有的線程池和連接池。 這一點很多同學,甚至在我經歷的團隊中就有人踩過坑, 每一個請求都創建一個 OkHttpClient 導致內存爆掉。

2、 從上面的整體框架圖,其實執行層有很多屬性功能是需要OkHttpClient 來制定,例如緩存、線程池、攔截器等。如果你是設計者你會怎樣設計 OkHttpClient ? 建造者模式,OkHttpClient 比較複雜, 太多屬性, 而且客戶的組合需求多樣化, 這種情況下就考慮使用建造者模式。 new OkHttpClien() 創建對象, 內部默認指定了很多屬性:


  1. public OkHttpClient() {
  2. this(new Builder());
  3. }

在看看 new Builder() 的默認實現:


  1. public Builder() {
  2. dispatcher = new Dispatcher();
  3. protocols = DEFAULT_PROTOCOLS;
  4. connectionSpecs = DEFAULT_CONNECTION_SPECS;
  5. eventListenerFactory = EventListener.factory(EventListener.NONE);
  6. proxySelector = ProxySelector.getDefault();
  7. cookieJar = CookieJar.NO_COOKIES;
  8. socketFactory = SocketFactory.getDefault();
  9. hostnameVerifier = OkHostnameVerifier.INSTANCE;
  10. certificatePinner = CertificatePinner.DEFAULT;
  11. proxyAuthenticator = Authenticator.NONE;
  12. authenticator = Authenticator.NONE;
  13. connectionPool = new ConnectionPool();
  14. dns = Dns.SYSTEM;
  15. followSslRedirects = true;
  16. followRedirects = true;
  17. retryOnConnectionFailure = true;
  18. connectTimeout = 10_000;
  19. readTimeout = 10_000;
  20. writeTimeout = 10_000;
  21. pingInterval = 0;
  22. }

默認指定 Dispatcher (管理線程池)、鏈接池、超時時間等。

3、 內部對於線程池、鏈接池管理有默認的管理策略,例如空閒時候的線程池、連接池會在一定時間自動釋放,但如果你想主動去釋放也可以通過客戶層去釋放。(很少)

執行層

Response response = mOkHttpClient.newCall(request).execute();

這是應用程序中發起網絡請求最頂端的調用,newCall(request) 方法返回 RealCall 對象。RealCall 封裝了一個 request 代表一個請求調用任務,RealCall 有兩個重要的方法 execute() 和 enqueue(Callback responseCallback)。 execute() 是直接在當前線程執行請求,enqueue(Callback responseCallback) 是將當前任務加到任務隊列中,執行異步請求。

同步請求


  1. @Override public Response execute() throws IOException {
  2. synchronized (this) {
  3. if (executed) throw new IllegalStateException("Already Executed");
  4. executed = true;
  5. }
  6. captureCallStackTrace();
  7. try {
  8. // client.dispatcher().executed(this) 內部只是記錄下執行狀態,
  9. client.dispatcher().executed(this);
  10. // 真正執行發生在這裡
  11. Response result = getResponseWithInterceptorChain();
  12. if (result == null) throw new IOException("Canceled");
  13. return result;
  14. } finally {
  15. // 後面再解釋
  16. client.dispatcher().finished(this);
  17. }
  18. }

執行方法關鍵在 getResponseWithInterceptorChain() 這個方法中, 關於 client.dispatcher().executed(this) 和 client.dispatcher().finished(this); 這裡先忽略 ,後面再看。

請求過程要從執行層說到連接層,涉及到 getResponseWithInterceptorChain 方法中組織的各個攔截器的執行過程,內容比較多,後面章節在說。先說說 RealCall 中 enqueue(Callback responseCallback) 方法涉及的異步請求和線程池。

Dispatcher 和線程池


  1. @Override public void enqueue(Callback responseCallback) {
  2. synchronized (this) {
  3. if (executed) throw new IllegalStateException("Already Executed");
  4. executed = true;
  5. }
  6. captureCallStackTrace();
  7. client.dispatcher().enqueue(new AsyncCall(responseCallback));
  8. }

調用了 dispatcher 的 enqueue()方法

dispatcher 結合線程池完成了所有異步請求任務的調配。

synchronized void enqueue(AsyncCall call) {

if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {

runningAsyncCalls.add(call);

executorService().execute(call);

} else {

readyAsyncCalls.add(call);

}

}

OKHTTP3源碼和設計模式(上篇)

dispatcher 主要維護了三兩個隊列 readyAsyncCalls、runningAsyncCalls 和 runningSyncCalls,分別代表了準備中隊列, 正在執行的異步任務隊列和正在執行的同步隊列, 重點關注下前面兩個。

現在我們可以回頭來看看前面 RealCall 方法 client.dispatcher().finished(this) 這個疑點了。

OKHTTP3源碼和設計模式(上篇)

在每個任務執行完之後要回調 client.dispatcher().finished(this) 方法, 主要是要將當前任務從 runningAsyncCalls 或 runningSyncCalls 中移除, 同時把 readyAsyncCalls 的任務調度到 runningAsyncCalls 中並執行。

線程池


  1. public synchronized ExecutorService executorService() {
  2. if (executorService == null) {
  3. executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
  4. new SynchronousQueue(), Util.threadFactory("OkHttp Dispatcher", false));
  5. }
  6. return executorService;
  7. }

默認實現是一個不限容量的線程池 , 線程空閒時存活時間為 60 秒。線程池實現了對象複用,降低線程創建開銷,從設計模式上來講,使用了享元模式。

責任鏈 (攔截器執行過程)


  1. Response getResponseWithInterceptorChain() throws IOException {
  2. // Build a full stack of interceptors.
  3. List interceptors = new ArrayList<>();
  4. interceptors.addAll(client.interceptors());
  5. interceptors.add(retryAndFollowUpInterceptor);
  6. interceptors.add(new BridgeInterceptor(client.cookieJar()));
  7. interceptors.add(new CacheInterceptor(client.internalCache()));
  8. interceptors.add(new ConnectInterceptor(client));
  9. if (!forWebSocket) {
  10. interceptors.addAll(client.networkInterceptors());
  11. }
  12. interceptors.add(new CallServerInterceptor(forWebSocket));
  13. Interceptor.Chain chain = new RealInterceptorChain(
  14. interceptors, null, null, null, 0, originalRequest);
  15. return chain.proceed(originalRequest);
  16. }
  17. }

要跟蹤 Okhttp3 的網絡請求任務執行過程 ,需要看懂以上代碼,看懂以上代碼必須理解設計模式-責任鏈。在責任鏈模式裡,很多對象由每一個對象對其下家的引用而連接起來形成一條鏈。請求在這個鏈上傳遞,直到鏈上的某一個對象決定處理此請求。發出這個請求的客戶端並不知道鏈上的哪一個對象最終處理這個請求,這使得系統可以在不影響客戶端的情況下動態地重新組織和分配責任。 網絡請求過程,是比較典型的複合責任鏈的場景,比如請求傳遞過程,我們需要做請求重試, 需要執行緩存策略, 需要建立連接等, 每一個處理節點可以由一個鏈上的對象來處理; 同時客戶端使用的時候可能也會在請求過程中做一些應用層需要的事情,比如我要記錄網絡請求的耗時、日誌等, 責任鏈還可以動態的擴展到客戶業務方。

OKHTTP3源碼和設計模式(上篇)

在 OkHttp3 的攔截器鏈中, 內置了5個默認的攔截器,分別用於重試、請求對象轉換、緩存、鏈接、網絡讀寫。

以上方法中先是添加了客戶端自定義的連接器,然後在分別添加內置攔截器。

Okhttp3 攔截器類圖

OKHTTP3源碼和設計模式(上篇)

現在我們把對 OkHttp 網絡請求執行過程的研究轉化對每個攔截器處理的研究。

retryAndFollowUpInterceptor 重試機制

OKHTTP3源碼和設計模式(上篇)

retryAndFollowUpInterceptor 處於內置攔截器鏈的最頂端,在一個循環中執行重試過程:

1、首先下游攔截器在處理網絡請求過程如拋出異常,則通過一定的機制判斷一下當前鏈接是否可恢復的(例如,異常是不是致命的、有沒有更多的線路可以嘗試等),如果可恢復則重試,否則跳出循環。

2、 如果沒什麼異常則校驗下返回狀態、代理鑑權、重定向等,如果需要重定向則繼續,否則直接跳出循環返回結果。

3、 如果重定向,則要判斷下是否已經達到最大可重定向次數, 達到則拋出異常,跳出循環。

@Override public Response intercept(Chain chain) throws IOException {

Request request = chain.request();

// 創建連接池管理對象

streamAllocation = new StreamAllocation(

client.connectionPool(), createAddress(request.url()), callStackTrace);


  1. int followUpCount = 0;
  2. Response priorResponse = null;
  3. while (true) {
  4. if (canceled) {
  5. streamAllocation.release();
  6. throw new IOException("Canceled");
  7. }
  8. Response response = null;
  9. boolean releaseConnection = true;
  10. try {
  11. // 將請求處理傳遞下游攔截器處理
  12. response = ((RealInterceptorChain) chain).proceed(request, streamAllocation, null, null);
  13. releaseConnection = false;
  14. } catch (RouteException e) {
  15. // The attempt to connect via a route failed. The request will not have been sent.
  16. // 線路異常,判斷滿足可恢復條件,滿足則繼續循環重試
  17. if (!recover(e.getLastConnectException(), false, request)) {
  18. throw e.getLastConnectException();
  19. }
  20. releaseConnection = false;
  21. continue;
  22. } catch (IOException e) {
  23. // An attempt to communicate with a server failed. The request may have been sent.

// IO異常,判斷滿足可恢復條件,滿足則繼續循環重試

boolean requestSendStarted = !(e instanceof ConnectionShutdownException);

if (!recover(e, requestSendStarted, request)) throw e;

releaseConnection = false;

continue;

} finally {

// We’re throwing an unchecked exception. Release any resources.

if (releaseConnection) {

streamAllocation.streamFailed(null);

streamAllocation.release();

}

}


  1. // Attach the prior response if it exists. Such responses never have a body.
  2. if (priorResponse != null) {
  3. response = response.newBuilder()
  4. .priorResponse(priorResponse.newBuilder()
  5. .body(null)
  6. .build())
  7. .build();
  8. }
  9. // 是否需要重定向
  10. Request followUp = followUpRequest(response);
  11. if (followUp == null) {
  12. if (!forWebSocket) {
  13. streamAllocation.release();
  14. }
  15. // 不需要重定向,正常返回結果
  16. return response;
  17. }
  18. closeQuietly(response.body());
  19. if (++followUpCount > MAX_FOLLOW_UPS) {
  20. // 達到次數限制
  21. streamAllocation.release();
  22. throw new ProtocolException("Too many follow-up requests: " + followUpCount);
  23. }
  24. if (followUp.body() instanceof UnrepeatableRequestBody) {
  25. streamAllocation.release();
  26. throw new HttpRetryException("Cannot retry streamed HTTP body", response.code());
  27. }
  28. if (!sameConnection(response, followUp.url())) {
  29. streamAllocation.release();
  30. streamAllocation = new StreamAllocation(
  31. client.connectionPool(), createAddress(followUp.url()), callStackTrace);
  32. } else if (streamAllocation.codec() != null) {
  33. throw new IllegalStateException("Closing the body of " + response
  34. + " didn't close its backing stream. Bad interceptor?");
  35. }
  36. request = followUp;
  37. priorResponse = response;
  38. }
  39. }

BridgeInterceptor


  1. /**
  2. * Bridges from application code to network code. First it builds a
  3. network request from a user
  4. * request. Then it proceeds to call the network. Finally it builds a
  5. user response from the network
  6. * response.
  7. */

這個攔截器比較簡單, 一個實現應用層和網絡層直接的數據格式編碼的橋。 第一: 把應用層客戶端傳過來的請求對象轉換為 Http 網絡協議所需字段的請求對象。 第二, 把下游網絡請求結果轉換為應用層客戶所需要的響應對象。 這個設計思想來自適配器設計模式,大家可以去體會一下。

CacheInterceptor 數據策略(策略模式)

CacheInterceptor 實現了數據的選擇策略, 來自網絡還是來自本地? 這個場景也是比較契合策略模式場景, CacheInterceptor 需要一個策略提供者提供它一個策略(錦囊), CacheInterceptor 根據這個策略去選擇走網絡數據還是本地緩存。

OKHTTP3源碼和設計模式(上篇)

緩存的策略過程:

1、 請求頭包含 “If-Modified-Since” 或 “If-None-Match” 暫時不走緩存

2、 客戶端通過 cacheControl 指定了無緩存,不走緩存

3、客戶端通過 cacheControl 指定了緩存,則看緩存過期時間,符合要求走緩存。

4、 如果走了網絡請求,響應狀態碼為 304(只有客戶端請求頭包含 “If-Modified-Since” 或 “If-None-Match” ,服務器數據沒變化的話會返回304狀態碼,不會返回響應內容), 表示客戶端繼續用緩存。

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

if (cache != null) {

cache.trackResponse(strategy);

}

if (cacheCandidate != null && cacheResponse == null) {

closeQuietly(cacheCandidate.body()); // The cache candidate wasn’t applicable. Close it.

}

// If we’re forbidden from using the network and the cache is insufficient, fail.

if (networkRequest == null && cacheResponse == null) {

return new Response.Builder()

.request(chain.request())

.protocol(Protocol.HTTP_1_1)

.code(504)

.message(“Unsatisfiable Request (only-if-cached)”)

.body(Util.EMPTY_RESPONSE)

.sentRequestAtMillis(-1L)

.receivedResponseAtMillis(System.currentTimeMillis())

.build();

}

// 走緩存

if (networkRequest == null) {

return cacheResponse.newBuilder()

.cacheResponse(stripBody(cacheResponse))

.build();

}

Response networkResponse = null;

try {

// 執行網絡

networkResponse = chain.proceed(networkRequest);

} finally {

// If we’re crashing on I/O or otherwise, don’t leak the cache body.

if (networkResponse == null && cacheCandidate != null) {

closeQuietly(cacheCandidate.body());

}

}


  1. // 返回 304 仍然走本地緩存
  2. if (cacheResponse != null) {
  3. if (networkResponse.code() == HTTP_NOT_MODIFIED) {
  4. Response response = cacheResponse.newBuilder()
  5. .headers(combine(cacheResponse.headers(), networkResponse.headers()))
  6. .sentRequestAtMillis(networkResponse.sentRequestAtMillis())
  7. .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
  8. .cacheResponse(stripBody(cacheResponse))
  9. .networkResponse(stripBody(networkResponse))
  10. .build();
  11. networkResponse.body().close();
  12. // Update the cache after combining headers but before stripping the
  13. // Content-Encoding header (as performed by initContentStream()).
  14. cache.trackConditionalCacheHit();
  15. cache.update(cacheResponse, response);
  16. return response;
  17. } else {
  18. closeQuietly(cacheResponse.body());
  19. }
  20. }
  21. Response response = networkResponse.newBuilder()
  22. .cacheResponse(stripBody(cacheResponse))
  23. .networkResponse(stripBody(networkResponse))
  24. .build();
  25. if (cache != null) {
  26. if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
  27. // 存儲緩存
  28. CacheRequest cacheRequest = cache.put(response);
  29. return cacheWritingResponse(cacheRequest, response);
  30. }
  31. if (HttpMethod.invalidatesCache(networkRequest.method())) {
  32. try {
  33. cache.remove(networkRequest);
  34. } catch (IOException ignored) {
  35. // The cache cannot be written.
  36. }
  37. }
  38. }
  39. return response;
  40. }

緩存實現

OkHttp3 內部緩存默認實現是使用的 DiskLruCache, 這部分代碼有點繞:

interceptors.add(new CacheInterceptor(client.internalCache()));

初始化 CacheInterceptor 時候 client.internalCache() 這裡獲取OkHttpClient的緩存。


  1. InternalCache internalCache() {
  2. return cache != null ? cache.internalCache : internalCache;
  3. }

注意到, 這個方法是非公開的。 客戶端只能通過 OkhttpClient.Builder的 cache(cache) 定義緩存, cache 是一個 Cache 對實例。 在看看 Cache 的內部實現, 內部有一個 InternalCache 的內部類實現。 內部調用時使用 InternalCache 實例提供接口,而存儲邏輯在 Cache 中實現。

OKHTTP3源碼和設計模式(上篇)

Cache 為什麼不直接實現 InternalCache ,而通過持有 InternalCache 的一個內部類對象來實現方法? 是希望控制緩存實現, 不希望用戶外部去實現緩存,同時對內保持一定的擴展。

鏈接層

RealCall 封裝了請求過程, 組織了用戶和內置攔截器,其中內置攔截器 retryAndFollowUpInterceptor -> BridgeInterceptor -> CacheInterceptor 完執行層的大部分邏輯 ,ConnectInterceptor -> CallServerInterceptor 兩個攔截器開始邁向連接層最終完成網絡請求。


分享到:


相關文章: