標籤:c# get lin tle 文檔 使用 提升 對象 解決
安卓開發領域,很多重要的問題都有了很好的開源解決方案,例如網路請求 OkHttp + Retrofit 簡直就是不二之選。“我們不重複造輪子不表示我們不需要知道輪子該怎麼造及如何更好的造!”,在用了這些好輪子將近兩年之後,現在是時候拆開輪子一探究竟了。本文基於 OkHttp 截至 2016.7.11 的最新源碼對其進行了詳細分析。
1,整體思路
從使用方法出發,首先是怎麼使用,其次是我們使用的功能在內部是如何?的,實現方案上有什麼技巧,有什麼範式。全文基本上是對 OkHttp 源碼的一個分析與導讀,非常建議大家下載 OkHttp 源碼之後,跟著本文,過一遍源碼。對於技巧和範式,由於目前我的功力還不到位,分析內容沒多少,歡迎大家和我一起討論。
首先放一張完整流程圖(看不懂沒關係,慢慢往後看):
2,基本用例
來自 OkHttp 官方網站。
2.1,建立 OkHttpClient 對象[代碼]java代碼:
OkHttpClient client = new OkHttpClient();
咦,怎麼不見 builder?莫急,且看其建構函式:
[代碼]java代碼:
public OkHttpClient() { this(new Builder());}
原來是方便我們使用,提供了一個“快捷操作”,全部使用了預設的配置。OkHttpClient.Builder 類成員很多,後面我們再慢慢分析,這裡先暫時略過:
[代碼]java代碼:
public Builder() { dispatcher = new Dispatcher(); protocols = DEFAULT_PROTOCOLS; connectionSpecs = DEFAULT_CONNECTION_SPECS; 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;}
2.2,發起 HTTP 要求[代碼]java代碼:
String run(String url) throws IOException { Request request = new Request.Builder() .url(url) .build(); Response response = client.newCall(request).execute(); return response.body().string();}
OkHttpClient 實現了 Call.Factory,負責根據請求建立新的 Call,在 拆輪子系列:拆 Retrofit中我們曾和它發生過一次短暫的遭遇:
callFactory 負責建立 HTTP 要求,HTTP 要求被抽象為了 okhttp3.Call 類,它表示一個已經準備好,可以隨時執行的 HTTP 要求
那我們現在就來看看它是如何建立 Call 的:
[代碼]java代碼:
/** * Prepares the {@code request} to be executed at some point in the future. */@Override public Call newCall(Request request) { return new RealCall(this, request);}
如此看來功勞全在 RealCall 類了,下面我們一邊分析同步網路請求的過程,一邊瞭解 RealCall 的具體內容。
2.2.1,同步網路請求
我們首先看 RealCall#execute:
[代碼]java代碼:
@Override public Response execute() throws IOException { synchronized (this) { if (executed) throw new IllegalStateException("Already Executed"); // (1) executed = true; } try { client.dispatcher().executed(this); // (2) Response result = getResponseWithInterceptorChain(); // (3) if (result == null) throw new IOException("Canceled"); return result; } finally { client.dispatcher().finished(this); // (4) }}
這裡我們做了 4 件事:
- 檢查這個 call 是否已經被執行了,每個 call 只能被執行一次,如果想要一個完全一樣的 call,可以利用
call#clone 方法進行複製。
- 利用
client.dispatcher().executed(this) 來進行實際執行,dispatcher 是剛才看到的OkHttpClient.Builder 的成員之一,它的文檔說自己是非同步 HTTP 要求的執行策略,現在看來,同步請求它也有摻和。
- 調用
getResponseWithInterceptorChain() 函數擷取 HTTP 返回結果,從函數名可以看出,這一步還會進行一系列“攔截”操作。
- 最後還要通知
dispatcher 自己已經執行完畢。
dispatcher 這裡我們不過度關注,在同步執行的流程中,涉及到 dispatcher 的內容只不過是告知它我們的執行狀態,比如開始執行了(調用 executed),比如執行完畢了(調用 finished),在非同步執行流程中它會有更多的參與。
真正發出網路請求,解析返回結果的,還是 getResponseWithInterceptorChain:
[代碼]java代碼:
private Response getResponseWithInterceptorChain() throws IOException { // Build a full stack of interceptors. List 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 (!retryAndFollowUpInterceptor.isForWebSocket()) { interceptors.addAll(client.networkInterceptors()); } interceptors.add(new CallServerInterceptor( retryAndFollowUpInterceptor.isForWebSocket())); Interceptor.Chain chain = new RealInterceptorChain( interceptors, null, null, null, 0, originalRequest); return chain.proceed(originalRequest);}
在 OkHttp 開發人員之一介紹 OkHttp 的文章裡面,作者講到:
the whole thing is just a stack of built-in interceptors.
可見 Interceptor 是 OkHttp 最核心的一個東西,不要誤以為它只負責攔截請求進行一些額外的處理(例如 cookie),實際上它把實際的網路請求、緩衝、透明壓縮等功能都統一了起來,每一個功能都只是一個 Interceptor,它們再串連成一個 Interceptor.Chain,環環相扣,最終圓滿完成一次網路請求。
從 getResponseWithInterceptorChain 函數我們可以看到,Interceptor.Chain 的分布依次是:
- 在配置
OkHttpClient 時設定的 interceptors;
- 負責失敗重試以及重新導向的
RetryAndFollowUpInterceptor;
- 負責把使用者構造的請求轉換為發送到伺服器的請求、把伺服器返回的響應轉換為方便使用的響應的
BridgeInterceptor;
- 負責讀取緩衝直接返回、更新緩衝的
CacheInterceptor;
- 負責和伺服器建立串連的
ConnectInterceptor;
- 配置
OkHttpClient 時設定的 networkInterceptors;
- 負責向伺服器發送請求資料、從伺服器讀取響應資料的
CallServerInterceptor。
在這裡,位置決定了功能,最後一個 Interceptor 一定是負責和伺服器實際通訊的,重新導向、緩衝等一定是在實際通訊之前的。
責任鏈模式在這個 Interceptor 鏈條中得到了很好的實踐(感謝 Stay 一語道破,自愧弗如)。
它包含了一些命令對象和一系列的處理對象,每一個處理對象決定它能處理哪些命令對象,它也知道如何將它不能處理的命令對象傳遞給該鏈中的下一個處理對象。該模式還描述了往該處理鏈的末尾添加新的處理對象的方法。
對於把 Request 變成 Response 這件事來說,每個 Interceptor 都可能完成這件事,所以我們循著鏈條讓每個 Interceptor 自行決定能否完成任務以及怎麼完成任務(自力更生或者交給下一個Interceptor)。這樣一來,完成網路請求這件事就徹底從 RealCall 類中剝離了出來,簡化了各自的責任和邏輯。兩個字:優雅!
責任鏈模式在安卓系統中也有比較典型的實踐,例如 view 系統對點擊事件(TouchEvent)的處理,具體可以參考Android設計模式源碼解析之責任鏈模式中相關的分析。
回到 OkHttp,在這裡我們先簡單分析一下 ConnectInterceptor 和 CallServerInterceptor,看看 OkHttp 是怎麼進行和伺服器的實際通訊的。
2.2.1.1,建立串連:
ConnectInterceptor[代碼]java代碼:
@Override public Response intercept(Chain chain) throws IOException { RealInterceptorChain realChain = (RealInterceptorChain) chain; Request request = realChain.request(); StreamAllocation streamAllocation = realChain.streamAllocation(); // We need the network to satisfy this request. Possibly for validating a conditional GET. boolean doExtensiveHealthChecks = !request.method().equals("GET"); HttpCodec httpCodec = streamAllocation.newStream(client, doExtensiveHealthChecks); RealConnection connection = streamAllocation.connection(); return realChain.proceed(request, streamAllocation, httpCodec, connection);}
實際上建立串連就是建立了一個 HttpCodec 對象,它將在後面的步驟中被使用,那它又是何方神聖呢?它是對 HTTP 協議操作的抽象,有兩個實現:Http1Codec 和 Http2Codec,顧名思義,它們分別對應 HTTP/1.1 和 HTTP/2 版本的實現。
在 Http1Codec 中,它利用 Okio 對 Socket 的讀寫操作進行封裝,Okio 以後有機會再進行分析,現在讓我們對它們保持一個簡單地認識:它對 java.io 和 java.nio 進行了封裝,讓我們更便捷高效的進行 IO 操作。
而建立 HttpCodec 對象的過程涉及到 StreamAllocation、RealConnection,代碼較長,這裡就不展開,這個過程概括來說,就是找到一個可用的 RealConnection,再利用 RealConnection 的輸入輸出(BufferedSource 和 BufferedSink)建立 HttpCodec 對象,供後續步驟使用。
2.2.1.2,發送和接收資料:
CallServerInterceptor[代碼]java代碼:
@Override public Response intercept(Chain chain) throws IOException { HttpCodec httpCodec = ((RealInterceptorChain) chain).httpStream(); StreamAllocation streamAllocation = ((RealInterceptorChain) chain).streamAllocation(); Request request = chain.request(); long sentRequestMillis = System.currentTimeMillis(); httpCodec.writeRequestHeaders(request); if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) { Sink requestBodyOut = httpCodec.createRequestBody(request, request.body().contentLength()); BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut); request.body().writeTo(bufferedRequestBody); bufferedRequestBody.close(); } httpCodec.finishRequest(); Response response = httpCodec.readResponseHeaders() .request(request) .handshake(streamAllocation.connection().handshake()) .sentRequestAtMillis(sentRequestMillis) .receivedResponseAtMillis(System.currentTimeMillis()) .build(); if (!forWebSocket || response.code() != 101) { response = response.newBuilder() .body(httpCodec.openResponseBody(response)) .build(); } if ("close".equalsIgnoreCase(response.request().header("Connection")) || "close".equalsIgnoreCase(response.header("Connection"))) { streamAllocation.noNewStreams(); } // 省略部分檢查代碼 return response;}
我們抓住主幹部分:
- 向伺服器發送 request header;
- 如果有 request body,就向伺服器發送;
- 讀取 response header,先構造一個
Response 對象;
- 如果有 response body,就在 3 的基礎上加上 body 構造一個新的
Response 對象;
這裡我們可以看到,核心工作都由 HttpCodec 對象完成,而 HttpCodec 實際上利用的是 Okio,而 Okio 實際上還是用的 Socket,所以沒什麼神秘的,只不過一層套一層,層數有點多。
其實 Interceptor 的設計也是一種分層的思想,每個 Interceptor 就是一層。為什麼要套這麼多層呢?分層的思想在 TCP/IP 協議中就體現得淋漓盡致,分層簡化了每一層的邏輯,每層只需要關注自己的責任(單一原則思想也在此體現),而各層之間通過約定的介面/協議進行合作(面向介面編程思想),共同完成複雜的任務。
簡單應該是我們的終極追求之一,儘管有時為了達成目標不得不複雜,但如果有另一種更簡單的方式,我想應該沒有人不願意替換。
2.2.2,發起非同步網路請求[代碼]java代碼:
client.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { } @Override public void onResponse(Call call, Response response) throws IOException { System.out.println(response.body().string()); }});// RealCall#enqueue@Override public void enqueue(Callback responseCallback) { synchronized (this) { if (executed) throw new IllegalStateException("Already Executed"); executed = true; } client.dispatcher().enqueue(new AsyncCall(responseCallback));}// Dispatcher#enqueuesynchronized void enqueue(AsyncCall call) { if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) { runningAsyncCalls.add(call); executorService().execute(call); } else { readyAsyncCalls.add(call); }}
這裡我們就能看到 dispatcher 在非同步執行時發揮的作用了,如果當前還能執行一個並發請求,那就立即執行,否則加入 readyAsyncCalls 隊列,而正在執行的請求執行完畢之後,會調用promoteCalls() 函數,來把 readyAsyncCalls 隊列中的 AsyncCall “提升”為runningAsyncCalls,並開始執行。
這裡的 AsyncCall 是 RealCall 的一個內部類,它實現了 Runnable,所以可以被提交到ExecutorService 上執行,而它在執行時會調用 getResponseWithInterceptorChain() 函數,並把結果通過 responseCallback 傳遞給上層使用者。
這樣看來,同步請求和非同步請求的原理是一樣的,都是在 getResponseWithInterceptorChain() 函數中通過 Interceptor 鏈條來實現的網路請求邏輯,而非同步則是通過 ExecutorService 實現。
2.3,返回資料的擷取
在上述同步(Call#execute() 執行之後)或者非同步(Callback#onResponse() 回調中)請求完成之後,我們就可以從 Response 對象中擷取到響應資料了,包括 HTTP status code,status message,response header,response body 等。這裡 body 部分最為特殊,因為伺服器返回的資料可能非常大,所以必須通過資料流的方式來進行訪問(當然也提供了諸如 string() 和 bytes() 這樣的方法將流內的資料一次性讀取完畢),而響應中其他部分則可以隨意擷取。
響應 body 被封裝到 ResponseBody 類中,該類主要有兩點需要注意:
- 每個 body 只能被消費一次,多次消費會拋出異常;
- body 必須被關閉,否則會發生資源泄漏;
在 2.2.1.2,發送和接收資料:CallServerInterceptor 小節中,我們就看過了 body 相關的代碼:
[代碼]java代碼:
if (!forWebSocket || response.code() != 101) { response = response.newBuilder() .body(httpCodec.openResponseBody(response)) .build();}
由 HttpCodec#openResponseBody 提供具體 HTTP 協議版本的響應 body,而 HttpCodec 則是利用 Okio 實現具體的資料 IO 操作。
這裡有一點值得一提,OkHttp 對響應的校正非常嚴格,HTTP status line 不能有任何雜亂的資料,否則就會拋出異常,在我們公司項目的實踐中,由於伺服器的問題,偶爾 status line 會有額外資料,而服務端的問題也毫無頭緒,導致我們不得不忍痛繼續使用 HttpUrlConnection,而後者在一些系統上又存在各種其他的問題,例如魅族系統發送 multi-part form 的時候就會出現沒有響應的問題。
2.4,HTTP 緩衝
在 2.2.1,同步網路請求 小節中,我們已經看到了 Interceptor 的布局,在建立串連、和伺服器通訊之前,就是 CacheInterceptor,在建立串連之前,我們檢查響應是否已經被緩衝、緩衝是否可用,如果是則直接返回緩衝的資料,否則就進行後面的流程,並在返回之前,把網路的資料寫入緩衝。
這塊代碼比較多,但也很直觀,主要涉及 HTTP 協議緩衝細節的實現,而具體的緩衝邏輯 OkHttp 內建封裝了一個 Cache 類,它利用 DiskLruCache,用磁碟上的有限大小空間進行緩衝,按照 LRU 演算法進行緩衝淘汰,這裡也不再展開。
我們可以在構造 OkHttpClient 時設定 Cache 對象,在其建構函式中我們可以指定目錄和緩衝大小:
[代碼]java代碼:
public Cache(File directory, long maxSize);
而如果我們對 OkHttp 內建的 Cache 類不滿意,我們可以自行實現 InternalCache 介面,在構造OkHttpClient 時進行設定,這樣就可以使用我們自訂的緩衝策略了。
3,總結
OkHttp 還有很多細節部分沒有在本文展開,例如 HTTP2/HTTPS 的支援等,但建立一個清晰的概覽非常重要。對整體有了清晰認識之後,細節部分如有需要,再單獨深入將更加容易。
在文章最後我們再來回顧一下完整的流程圖:
OkHttpClient 實現 Call.Factory,負責為 Request 建立 Call;
RealCall 為具體的 Call 實現,其 enqueue() 非同步介面通過 Dispatcher 利用ExecutorService 實現,而最終進行網路請求時和同步 execute() 介面一致,都是通過getResponseWithInterceptorChain() 函數實現;
getResponseWithInterceptorChain() 中利用 Interceptor 鏈條,分層實現緩衝、透明壓縮、網路 IO 等功能;
Android OkHttp使用與分析