天天看點

OKHTTP3源碼和設計模式(上篇)整體架構OkHttpClient執行層Dispatcher 和線程池責任鍊 (攔截器執行過程)緩存實作連結層

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

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

整體架構

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

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

OkHttpClient

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

/**
 * Factory for {@linkplain Call calls}, which can be used to send 
   HTTP requests and read their
 * responses.
 *
 * <h3>OkHttpClients should be shared</h3>
 *
 * <p>OkHttp performs best when you create a single {@code 
 OkHttpClient} instance and reuse it for
 * all of your HTTP calls. This is because each client holds its own 
connection pool and thread
 * pools. Reusing connections and threads reduces latency and 
saves memory. Conversely, creating a
 * client for each request wastes resources on idle pools.
 *
 * <p>Use {@code new OkHttpClient()} to create a shared instance 
with the default settings:
 * <pre>   {@code
 *
 *   // The singleton HTTP client.
 *   public final OkHttpClient client = new OkHttpClient();
 * }</pre>
 *
 * <p>Or use {@code new OkHttpClient.Builder()} to create a shared 
  instance with custom settings:
 * <pre>   {@code
 *
 *   // The singleton HTTP client.
 *   public final OkHttpClient client = new OkHttpClient.Builder()
 *       .addInterceptor(new HttpLoggingInterceptor())
 *       .cache(new Cache(cacheDir, cacheSize))
 *       .build();
 * }</pre>
 *
 ....  省略
*/
           

簡單提煉:

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

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

public OkHttpClient() {
   this(new Builder());
}
           

在看看 new Builder() 的預設實作:

public Builder() {
  dispatcher = new Dispatcher();
  protocols = DEFAULT_PROTOCOLS;
  connectionSpecs = DEFAULT_CONNECTION_SPECS;
  eventListenerFactory = EventListener.factory(EventListener.NONE);
  proxySelector = ProxySelector.getDefault();
  cookieJar = CookieJar.NO_COOKIES;
  socketFactory = SocketFactory.getDefault();
  hostnameVerifier = OkHostnameVerifier.INSTANCE;
  certificatePinner = CertificatePinner.DEFAULT;
  proxyAuthenticator = Authenticator.NONE;
  authenticator = Authenticator.NONE;
  connectionPool = new ConnectionPool();
  dns = Dns.SYSTEM;
  followSslRedirects = true;
  followRedirects = true;
  retryOnConnectionFailure = true;
  connectTimeout = 10_000;
  readTimeout = 10_000;
  writeTimeout = 10_000;
  pingInterval = 0;
}
           

預設指定 Dispatcher (管理線程池)、連結池、逾時時間等。

3、 内部對于線程池、連結池管理有預設的管理政策,例如空閑時候的線程池、連接配接池會在一定時間自動釋放,但如果你想主動去釋放也可以通過客戶層去釋放。(很少)

執行層

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

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

同步請求

@Override public Response execute() throws IOException {
  synchronized (this) {
    if (executed) throw new IllegalStateException("Already Executed");
    executed = true;
  }
  captureCallStackTrace();
  try {
    // client.dispatcher().executed(this) 内部隻是記錄下執行狀态,
    client.dispatcher().executed(this);
    // 真正執行發生在這裡
    Response result = getResponseWithInterceptorChain();
    if (result == null) throw new IOException("Canceled");
    return result;
  } finally {
    // 後面再解釋
    client.dispatcher().finished(this);
  }
}
           

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

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

Dispatcher 和線程池

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

調用了 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);

}

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

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

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

線程池

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

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

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

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);
return chain.proceed(originalRequest);
  }
}
           

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

在 OkHttp3 的攔截器鍊中, 内置了5個預設的攔截器,分别用于重試、請求對象轉換、緩存、連結、網絡讀寫。

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

Okhttp3 攔截器類圖

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

retryAndFollowUpInterceptor 重試機制

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

int followUpCount = 0;
Response priorResponse = null;
while (true) {
  if (canceled) {
    streamAllocation.release();
    throw new IOException("Canceled");
  }

  Response response = null;
  boolean releaseConnection = true;
  try {
  // 将請求處理傳遞下遊攔截器處理
    response = ((RealInterceptorChain) chain).proceed(request, streamAllocation, null, null);
    releaseConnection = false;
  } catch (RouteException e) {
    // The attempt to connect via a route failed. The request will not have been sent.
     //  線路異常,判斷滿足可恢複條件,滿足則繼續循環重試
    if (!recover(e.getLastConnectException(), false, request)) {
      throw e.getLastConnectException();
    }
    releaseConnection = false;
    continue;
  } catch (IOException e) {
    // 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();

// Attach the prior response if it exists. Such responses never have a body.
  if (priorResponse != null) {
    response = response.newBuilder()
        .priorResponse(priorResponse.newBuilder()
                .body(null)
                .build())
        .build();
  }
 //  是否需要重定向
  Request followUp = followUpRequest(response);

  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 (followUp.body() instanceof UnrepeatableRequestBody) {
    streamAllocation.release();
    throw new HttpRetryException("Cannot retry streamed HTTP body", response.code());
  }

  if (!sameConnection(response, followUp.url())) {
    streamAllocation.release();
    streamAllocation = new StreamAllocation(
        client.connectionPool(), createAddress(followUp.url()), callStackTrace);
  } 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;
 }
}
           

BridgeInterceptor

/**
  * Bridges from application code to network code. First it builds a 
network request from a user
 * request. Then it proceeds to call the network. Finally it builds a 
user response from the network
 * response.
 */
           

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

CacheInterceptor 資料政策(政策模式)

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

緩存的政策過程:

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

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

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

4、 如果走了網絡請求,響應狀态碼為 304(隻有用戶端請求頭包含 “If-Modified-Since” 或 “If-None-Match” ,伺服器資料沒變化的話會傳回304狀态碼,不會傳回響應内容), 表示用戶端繼續用緩存。

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

Response networkResponse = null;

try {

// 執行網絡

networkResponse = chain.proceed(networkRequest);

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

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

closeQuietly(cacheCandidate.body());

// 傳回 304 仍然走本地緩存
if (cacheResponse != null) {
  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();

    // Update the cache after combining headers but before stripping the
    // Content-Encoding header (as performed by initContentStream()).
    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) {
      // The cache cannot be written.
    }
  }
}
return response;
}
           

緩存實作

OkHttp3 内部緩存預設實作是使用的 DiskLruCache, 這部分代碼有點繞:

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

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

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

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

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

連結層

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

歡迎工作一到五年的Java工程師朋友們加入Java架構開發:468947140

點選連結加入群聊【Java-BATJ企業級資深架構】:https://jq.qq.com/?_wv=1027&k=5zMN6JB

本群提供免費的學習指導 架構資料 以及免費的解答

不懂得問題都可以在本群提出來 之後還會有職業生涯規劃以及面試指導