Java程式設計架構實戰——OKHTTP3原始碼和設計模式(上篇)

慕容千語發表於2019-02-14

本文來探究一下 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排程

dispatcher 主要維護了三兩個佇列 readyAsyncCalls、runningAsyncCalls 和 runningSyncCalls,分別代表了準備中佇列, 正在執行的非同步任務佇列和正在執行的同步佇列, 重點關注下前面兩個。
現在我們可以回頭來看看前面 RealCall 方法 client.dispatcher().finished(this) 這個疑點了。

Dispatcher排程

在每個任務執行完之後要回撥 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狀態碼,不會返回響應內容), 表示客戶端繼續用快取。

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

}

}

// 返回 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 兩個攔截器開始邁向連線層最終完成網路請求。

喜歡的關注下筆者,順便點個讚唄。

相關文章