文章首發在我的博客 https://www.nullobject.cn,公衆號 NullObject同步更新。
這篇文章主要介紹OkHttpClient、Request、Call、Dispatcher、Response等類文章基於OkHttp3.14.3版本html
上一篇OkHttp源碼分析(一)請求和響應過程簡單分析中咱們簡單分析了OkHttp從請求到響應的過程,這篇就來深刻學習下其中涉及到的比較關鍵的類:java
OkHttpClient類主要應用了外觀模式和建造者模式兩種設計模式來設計,結合外觀模式的思想,將許多對應OkHttp中各個功能模塊的對象包含到類中,併爲這些功能對象的配置提供了共同的對外接口。同時使用建造者模式,提供一個Builder類爲這些衆多的功能模塊提供鏈式的配置方式,使得繁雜的功能模塊配置變得簡潔。首先,建立一個OkHttpClient.builder對象,接着按需設置builder各個參數:git
OkHttpClient.Builder builder = new OkHttpClient.Builder();
OkHttpClient中超時時間參數有如下幾種:github
// 設置整個請求過程最大超時時間爲60s builder.callTimeout(Duratino.ofSeconds(60));
// 設置鏈接創建超時時間 builder.connectTimeout(Duration.ofSeconds(10));
// 設置讀超時時間 builder.readTimeout(Duration.ofSeconds(10));
// 設置寫超時時間 builder.writeTimeout(Duration.ofSeconds(10));
builder.retryOnConnectionFailure(true);
OkHttpClient支持添加多個HTTP/HTTPS請求攔截器和WebSocket攔截器:web
builder.addInterceptor(chain -> chain.proceed(chain.request()));
builder.addNetworkInterceptor(chain -> chain.proceed(chain.request()));
OkHttp支持自定義緩存的路徑和大小,以及Cookie的緩存處理:設計模式
// 設置緩存文件,用於將HTTP/HTTPS響應緩存到文件系統從而達到重用的目的以節省時間和網絡帶寬 builder.cache(new Cache(new File("cache_path"), 1024 * 1024))
// 將cookie緩存到內存中 builder.cookieJar(new CookieJar() { private final HashMap<String, List<Cookie>> cookieStore = new HashMap<>(); @Override public void saveFromResponse(final HttpUrl url, final List<Cookie> cookies) { cookieStore.put(url.host(), cookies); } @Override public List<Cookie> loadForRequest(final HttpUrl url) { List<Cookie> cookies = cookieStore.get(url.host()); return null != cookies ? cookies : new ArrayList<Cookie>(); } })
// 自定義dns解析,屏蔽百度用域名解析並使用系統提供的DNS解析服務解析其餘域名 builder.dns(hostname -> { // 屏蔽百度連接 if (hostname.contains("baidu.com")) { List<InetAddress> addresses = new ArrayList<>(); addresses.add(InetAddress.getByAddress(new byte[]{(byte) 127, (byte) 0, (byte) 0, (byte) 1})); return addresses; } return Dns.SYSTEM.lookup(hostname); })
builder.followRedirects(true) .followSslRedirects(true);
builder.pingInterval(Duration.ofSeconds(59));
builder.protocols(Util.immutableList(Protocol.HTTP_2, Protocol.HTTP_1_1));
// 手動配置鏈接池,其中ConnectionPool第一個參數表示池內容納的最大鏈接 builder.connectionPool(new ConnectionPool(5, 5, TimeUnit.MINUTES));
builder.dispatcher(new Dispatcher()); //手動配置執行請求任務的線程池 builder.dispatcher(new Dispatcher(Executors.newFixedThreadPool(64)));
OkHttp中的EventListener,對於每次請求,猶如"上帝視角"般的存在:若是爲OkHttpClient設置了EventListener,則一個請求從發起到結束的全部步驟都會被EventListener「看」到,請求的完整生命週期事件都會經過EventListener對應的接口回調給上層,所以,在開發debug階段,或想要了解一個請求須要經歷哪些流程時,也能夠經過設置EventListener來獲取相應信息。緩存
// EventListener.NONE不監放任何事件 builder.eventListenerFactory(call -> EventListener.NONE);
public Builder eventListener(EventListener eventListener) { if (eventListener == null) throw new NullPointerException("eventListener == null"); this.eventListenerFactory = EventListener.factory(eventListener); return this; }
能夠經過如下三個選項設置請求代理:安全
builder.proxySelector(ProxySelector.getDefault());
builder.proxyAuthenticator(Authenticator.NONE);
builder.proxy(Proxy.NO_PROXY);
builder.socketFactory(SocketFactory.getDefault());
// 設置默認固定證書 builder.certificatePinner(CertificatePinner.DEFAULT);
builder.connectionSpecs(Util.immutableList(ConnectionSpec.MODERN_TLS, ConnectionSpec.CLEARTEXT));
builder.sslSocketFactory((SSLSocketFactory) SSLSocketFactory.getDefault(), Util.platformTrustManager());
最後,生成OkHttpClient對象:服務器
OkHttpClient client = builder.build();
接下來看看Request類。websocket
Request 封裝了請求的內容,包括連接、請求體參數、請求頭等,以及請求tag,比較簡單。Request也是經過Builder構建:
// Step 2. 構建一個Request用於封裝請求地址、請求類型、參數等信息 Request request = new Request.Builder().get() .url("https://www.baidu.com") .build();
建立好OkHttpClient和Request以後,就能夠生成請求任務,發起請求了,接下來看Call和其實現類RealCall。
當前版本的OkHttp中,接口Call只有RealCall這一個實現類。Call表示一個已經準備好,能夠執行的請求任務,Call執行時能夠取消,但一個Call只能被執行一次。Call除了封裝分別用於執行同步和異步請求的execute()、enqueue(callback)兩個接口外,還封裝了其餘幾個請求相關的接口:
接下來看看RealCall類源碼。
咱們在發起Http請求時會經過OkHttpClient和Request對象來建立Call:
// 建立一個新的請求任務Call Call call = httpClient.newCall(request);
而httpClient.newCall()方法內部經過RealCall的靜態方法newCall建立並返回一個RealCall對象,因此在執行同步/異步請求時實際調用的是RealCall中的實現方法:
// OkHttpClient.newCall @Override public Call newCall(Request request) { return RealCall.newRealCall(this, request, false /* for web socket */); }
打開RealCall源碼能夠看到,RealCall內部持有本次請求的Request對象和OkHttpClient對象,同時還發現,RealCall還聲明瞭一個Transmitter類型的對象並隨着RealCall的建立而建立。Transmitter做爲OkHttp的應用層和網絡層的鏈接,負責對外暴露OkHttp中的高級應用程序層原語,包括鏈接、請求、響應和流等。結合RealCall源碼能夠發現,RealCall負責的是請求發起和執行,Transmitter則負責請求任務的狀態、超時時間、生命週期事件的更新以及請求任務背後的鏈接、鏈接池的維護管理等。
/*RealCall.java:經過transmitter控制超時時間的計算、生命週期步驟更新、取消請求等*/ @Override public Response execute() throws IOException { // ... 忽略其餘代碼 transmitter.timeoutEnter(); transmitter.callStart(); // ... 忽略其餘代碼 } @Override public void enqueue(Callback responseCallback) { // ... 忽略其餘代碼 transmitter.callStart(); // ... 忽略其餘代碼 } @Override public void cancel() { transmitter.cancel(); } @Override public Timeout timeout() { return transmitter.timeout(); }
再看看clone()方法實現:
@SuppressWarnings("CloneDoesntCallSuperClone") // We are a final type & this saves clearing state. @Override public RealCall clone() { return RealCall.newRealCall(client, originalRequest, forWebSocket); }
能夠看到,clone內部建立了一個新的Call,至關於調用了(RealCall)client.newCall(request)
,所以能夠用這個克隆的Call繼續發起請求。
RealCall還封裝了個內部類AsyncCall用於執行異步請求,AsyncCall聲明瞭CallBack變量用於回調通知異步請求結果,以及一個線程安全的AtomicInteger類型變量callsPerHost用於計量同一主機的請求數。經過上一篇的分析知道,AsyncCall是一個Runnable而且最終經過AsyncCall.execute()方法執行網絡請求。那RealCall內部是怎樣執行到這個這個方法的呢?咱們以前是經過快速跳轉實現的方式找到了這個方法的,而AsyncCall封裝的異步請求任務是在RealCall.enqueue執行時被添加到Dispatch中的請求隊列:
// RealCall.enqueue() @Override public void enqueue(Callback responseCallback) { // ... 忽略無關代碼 client.dispatcher().enqueue(new AsyncCall(responseCallback)); }
那麼只要搞清楚Dispatcher.enqueue()背後的隊列中的任務什麼時候何地執行的,上面的問題就有答案了。
Dispatcher是OkHttp中的請求任務調度器,內部維護了一個線程池和相關的請求隊列用於實現高併發的異步請求:
public final class Dispatcher { // 最大併發請求數,默認爲64個 private int maxRequests = 64; // 相同服務器主機的最大併發請求數,默認爲5個 private int maxRequestsPerHost = 5; // 空閒回調,若是設置,則當該調度器空閒時(正在執行的任務數變爲0)時回調通知idleCallback.run() private @Nullable Runnable idleCallback; // 執行異步任務的線程池 private @Nullable ExecutorService executorService; // 存放已經準備就緒能夠執行,但還沒有執行的異步請求任務的雙向隊列 private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>(); // 存放正在執行的異步請求任務,包括已經取消但未徹底結束的請求的雙向隊列 private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>(); // 存放正在執行的同步請求任務,包括已經取消但未徹底結束的請求的雙向隊列 private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>(); //...其餘代碼 }
其中線程池對象經過懶加載方式建立:
public synchronized ExecutorService executorService() { if (executorService == null) { executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS, new SynchronousQueue<>(), Util.threadFactory("OkHttp Dispatcher", false)); } return executorService; }
能夠看到,executorService實際建立的是一個無邊界、核心線程數爲0的線程池。其中池內線程空閒等待時長爲60s,超過空閒時間自動結束;工做隊列workQueue爲SynchronousQueue類型的隊列,該隊列是一個同步隊列,保證併發的任務順序執行,且該類型隊列內部不存儲值,只做傳遞,因爲executorService建立的線程數無限制,不會有隊列等待,因此使用SynchronousQueue(參考Executors.newCachedThread()建立緩存線程池);
參考 https://www.jianshu.com/p/074dff0f4ecb:SynchronousQueue每一個插入操做必須等待另外一個線程的移除操做,一樣任何一個移除操做都等待另外一個線程的插入操做。所以隊列內部其實沒有任何一個元素,或者說容量爲0,嚴格說並非一種容器,因爲隊列沒有容量,所以不能調用peek等操做,所以只有移除元素纔有元素,顯然這是一種快速傳遞元素的方式,也就是說在這種狀況下元素老是以最快的方式從插入者(生產者)傳遞給移除者(消費者),這在多任務隊列中最快的處理任務方式。對於高頻請求場景,無疑是最合適的。
在OKHttp中,建立了一個閥值是Integer.MAX_VALUE的線程池,它不保留任何最小線程,隨時建立更多的線程數,並且若是線程空閒後,只能多活60秒。因此也就說若是收到20個併發請求,線程池會建立20個線程,當完成後的60秒後會自動關閉全部20個線程。他這樣設計成不設上限的線程,以保證I/O任務中高阻塞低佔用的過程,不會長時間卡在阻塞上。
接着第3節RealCall分析最後的問題,來看看Dispatcher.enqueue()方法的實現:
// Dispatcher.enqueue() void enqueue(AsyncCall call) { synchronized (this) { // 將新的異步請求任務添加到readyAsyncCalls隊列 readyAsyncCalls.add(call); // 修改call,使其共享runningAsyncCalls或readyAsyncCalls中現有的請求相同主機的call.callsPerHost變量 if (!call.get().forWebSocket) { AsyncCall existingCall = findExistingCallWithHost(call.host()); if (existingCall != null) call.reuseCallsPerHostFrom(existingCall); } } // 執行AsyncCall請求調度策略,發起call請求 promoteAndExecute(); }
對於新請求入列的異步請求任務call,首先將其添加到readyAsyncCalls隊列,以表示這個call準備就緒,能夠執行請求;接着修改這個call的callsPerHost屬性爲與先前添加的相同主機請求任務的call共享,從而實現對相同主機的請求計數,對相同主機的最大併發請求數進行限制。接着調用promoteAndExecute()方法,將readyAsyncCalls隊列中的任務提高到runningAsyncCalls,並執行請求:
private boolean promoteAndExecute() { assert (!Thread.holdsLock(this)); // 建立可執行任務列表,用於篩選出readyAsyncCalls中可執行的任務 List<AsyncCall> executableCalls = new ArrayList<>(); boolean isRunning; synchronized (this) { // 遍歷篩選readyAsyncCalls for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) { AsyncCall asyncCall = i.next(); // 若是runningAsyncCalls中的請求任務數超過最大併發請求數限制maxRequests則任務繼續放在readyAsyncCalls中等待執行 if (runningAsyncCalls.size() >= maxRequests) break; // Max capacity. // 若是與asyncCall相同主機的請求數超過最大併發同主機請求數則,則不執行該請求 if (asyncCall.callsPerHost().get() >= maxRequestsPerHost) continue; // Host max capacity. // 從readyAsyncCalls中移除符合條件的請求任務 i.remove(); // 將asyncCall關聯的主機請求數增1 asyncCall.callsPerHost().incrementAndGet(); // 加入可執行請求任務列表 executableCalls.add(asyncCall); // 加入runningAsyncCalls任務隊列 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; }
能夠看到,Dispatcher請求調度器最終是在promoteAndExecute()方法中實現最大併發請求數量和最大併發同主機請求數量限制的。在方法的最後遍歷執行請求任務,調用了每個AsyncCall的executeOn()方法並將當前Dispatcher的線程池做爲參數傳入,在看看這個AsyncCall.executeOn(executorService)方法的實現:
// RealCall.AsyncCall.executeOn() void executeOn(ExecutorService executorService) { assert (!Thread.holdsLock(client.dispatcher())); boolean success = false; try { // 經過executorService線程池執行請求任務 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 { // 最後若是請求任務執行失敗則結束任務,從runningAsyncCalls中將任務移除 if (!success) { client.dispatcher().finished(this); // This call is no longer running! } } }
AsyncCall.executeOn()方法內部調用線程池的execute方法執行本次任務,由此觸發AsyncCall父類的run方法,並執行到AsyncCall的execute()方法,完成了本次請求!所以,AsyncCall.execute()調用過程大體以下:
最後,調用Dispatcher.finished(call)方法結束本次請求:
@Override protected void execute() { // ... 忽略無關代碼 try { // ... 忽略無關代碼 } catch (IOException e) { // ... 忽略無關代碼 } finally { // 結束本次請求 client.dispatcher().finished(this); } }
// Dispatcher.finished(call) void finished(AsyncCall call) { // 將call同主機併發請求數減1 call.callsPerHost().decrementAndGet(); // 結束本次請求任務,主動將call從runningAsyncCalls隊列移除 finished(runningAsyncCalls, call); }
以上從Dispatcher.enqueue()開始到Dispatcher.finished(call)結束就是Dispatcher調度異步請求的過程。Dispatcher對同步請求的調度執行就簡單多了,單線程任務,直接從RealCall.execute跟進分析便可。不論是同步仍是異步請求,最終都是經過RealCall的getResponseWithInterceptorChain()方法完成請求和獲取響應結果的,那getResponseWithInterceptorChain()方法內部是如何經過攔截器鏈完成請求的呢?下一篇就來分析分析OkHttp中的攔截器Interceptor。
歡迎關注我的公衆號: