OkHttp是什么?
簡介
OkHttp是一款優(yōu)秀的HTTP框架,它支持get請求和post請求,支持基于Http的文件上傳和下載,支持加載圖片,支持下載文件透明的GZIP壓縮,支持響應(yīng)緩存避免重復(fù)的網(wǎng)絡(luò)請求,支持使用連接池來降低響應(yīng)延遲問題。OkHttp由Square公司開發(fā),是目前Android最熱門的網(wǎng)絡(luò)框架之一。
官網(wǎng)網(wǎng)址:OKHttp官網(wǎng)
Github地址:Github
特點
- 支持HTTP2/SPDY
- socket自動選擇最好路線,并支持自動重連
- 擁有自動維護(hù)的socket連接池,減少握手次數(shù)
- 擁有隊列線程池,輕松寫并發(fā)
- 擁有Interceptors輕松處理請求與響應(yīng)(比如透明GZIP壓縮)基于Headers的緩存策略
OkHttp怎么用?
1、gradle引入庫,implementation 'com.squareup.okhttp3:okhttp:3.11.0'
2、初始化OkHttpClient對象
client = new OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
.build();
同步請求
public void okHttpSync() {
Request request = new Request.Builder()
.url("https://www.baidu.com")
.build();
Call call = client.newCall(request);
try {
Response response = call.execute();
if (response.isSuccessful()) {
System.out.println("response.code()==" + response.code());
System.out.println("response.heard()==" + response.headers());
System.out.println("response.message()==" + response.message());
System.out.println("res==" + response.body().string());
}
} catch (IOException e) {
e.printStackTrace();
}
}
異步請求
public void okHttpAsync() {
Request request = new Request.Builder()
.url("https://www.baidu.com")
.build();
Call call = client.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
needCancelled.set(true);
System.out.println("url==" + call.request().url());
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (response.isSuccessful()) {
System.out.println("response.code()==" + response.code());
System.out.println("response.heard()==" + response.headers());
System.out.println("response.message()==" + response.message());
System.out.println("res==" + response.body().string());
needCancelled.set(true);
}
}
});
}
詳細(xì)的OkHttp使用可參考OKHttp使用詳解
OkHttp核心執(zhí)行流程是怎樣?
關(guān)鍵類功能說明
| 類 | 功能說明 |
|---|---|
| OKHttpClient | 里面包含了很多對象,OKhttp的很多功能模塊都包裝進(jìn)這個類,讓這個類單獨提供對外的API,使用Builder模型構(gòu)建 |
| Request、Response | 抽象的網(wǎng)絡(luò)輸入及響應(yīng)模型 |
| Call | HTTP請求任務(wù)封裝,是一個接口 |
| RealCall | Call的實現(xiàn),實現(xiàn)execute()同步方法、enqueue(Callback responseCallback)異步方法, getResponseWithInterceptorChain() 獲取攔截器響應(yīng) |
| AsyncCall | RealCall的內(nèi)部類。繼承了Runnable接口,后續(xù)在異步的線程池中執(zhí)行 |
| Dispatcher | 核心調(diào)度類,內(nèi)部維護(hù)為了readyAsyncCalls、runningAsyncCalls、runningSyncCalls隊列,實際RealCall后續(xù)也是調(diào)用該類進(jìn)行同步、異步的具體實現(xiàn)。內(nèi)部維護(hù)了一個線程池,限制了最大并發(fā)數(shù)maxRequests=64。 |
| RealInterceptorChain | 攔截器鏈,維護(hù)了一個interceptors隊列,每次proceed通過index + 1會執(zhí)行下一攔截器的intercept方法 |
| RetryAndFollowUpInterceptor | 負(fù)責(zé)失敗重連以及重定向 |
| BridgeInterceptor | 負(fù)責(zé)對Request和Response報文進(jìn)行加工 |
| CacheInterceptor | 負(fù)責(zé)緩存攔截器 |
| ConnectInterceptor | 負(fù)責(zé)維護(hù)連接攔截器 |
| CallServerInterceptor | 負(fù)責(zé)最后網(wǎng)絡(luò)IO的讀寫 |
代碼執(zhí)行流程
1、通過Builder模式統(tǒng)一構(gòu)建OkHttpClient對象
2、通過Call,實現(xiàn)類RealCall進(jìn)行請求發(fā)送
3、RealCall通過調(diào)用了Dispatcher的execute()及enqueue()方法進(jìn)行同步及異步的請求
4、最終調(diào)用ReallCall的getResponseWithInterceptorChain()方法進(jìn)行攔截鏈的攔截
5、依次通過重定向攔截器、橋接攔截器、緩存攔截器、連接攔截器、網(wǎng)絡(luò)攔截器依次進(jìn)行處理
6、最后通過intercept的return往回返回Response,最終返回給客戶端請求的結(jié)果
OkHttp如何進(jìn)行線程調(diào)度控制?
線程調(diào)度
在Dispatcher中維護(hù)了一個線程池,異步的請求會將任務(wù)加入到線程池中。
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;
}
默認(rèn)的最大并發(fā)數(shù)為maxRequests=64,如果超過限制會加入到等待隊列中,執(zhí)行異步的方法如下
synchronized void enqueue(AsyncCall call) {
if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
runningAsyncCalls.add(call);
executorService().execute(call);
} else {
readyAsyncCalls.add(call);
}
}
最后線程池執(zhí)行AsyncCall中的execute()方法,如下
@Override protected void execute() {
boolean signalledCallback = false;
try {
Response response = getResponseWithInterceptorChain();
if (retryAndFollowUpInterceptor.isCanceled()) {
signalledCallback = true;
responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
} else {
signalledCallback = true;
responseCallback.onResponse(RealCall.this, response);
}
} catch (IOException e) {
if (signalledCallback) {
// Do not signal the callback twice!
Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
} else {
eventListener.callFailed(RealCall.this, e);
responseCallback.onFailure(RealCall.this, e);
}
} finally {
client.dispatcher().finished(this);
}
}
隊列機制
Dispathcer中維護(hù)了3個隊列,分別為異步等待隊列、異步執(zhí)行隊列、同步執(zhí)行隊列。
/** Ready async calls in the order they'll be run. */
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
/** Running asynchronous calls. Includes canceled calls that haven't finished yet. */
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
/** Running synchronous calls. Includes canceled calls that haven't finished yet. */
private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
不管是同步還是異步,最終在finally塊都會調(diào)用dispatcher的finished方法,會移除掉該隊列任務(wù),最后實現(xiàn)如下
int runningCallsCount;
Runnable idleCallback;
synchronized (this) {
if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
if (promoteCalls) promoteCalls();
runningCallsCount = runningCallsCount();
idleCallback = this.idleCallback;
}
if (runningCallsCount == 0 && idleCallback != null) {
idleCallback.run();
}
在finish中會再調(diào)用promoteCalls方法,會重新檢索準(zhǔn)備中的隊列,將隊列加入到線程中
private void promoteCalls() {
if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.
if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote.
for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
AsyncCall call = i.next();
if (runningCallsForHost(call) < maxRequestsPerHost) {
i.remove();
runningAsyncCalls.add(call);
executorService().execute(call);
}
if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
}
}
OkHttp的攔截器及調(diào)用鏈?zhǔn)窃趺磮?zhí)行?
調(diào)用鏈執(zhí)行流程
通過上述的分析,我們知道不管同步還是異步,最終調(diào)用到的都是RealCall的getResponseWithInterceptorChain()方法,如下:
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, this, eventListener, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());
return chain.proceed(originalRequest);
}
其中定義了攔截器集合及RealInterceptorChain攔截鏈,具體執(zhí)行了攔截鏈的proceed方法,如下:
public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
RealConnection connection) throws IOException {
if (index >= interceptors.size()) throw new AssertionError();
calls++;
// If we already have a stream, confirm that the incoming request will use it.
if (this.httpCodec != null && !this.connection.supportsUrl(request.url())) {
throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
+ " must retain the same host and port");
}
// If we already have a stream, confirm that this is the only call to chain.proceed().
if (this.httpCodec != null && calls > 1) {
throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
+ " must call proceed() exactly once");
}
// Call the next interceptor in the chain.
RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
writeTimeout);
Interceptor interceptor = interceptors.get(index);
Response response = interceptor.intercept(next);
// Confirm that the next interceptor made its required call to chain.proceed().
if (httpCodec != null && index + 1 < interceptors.size() && next.calls != 1) {
throw new IllegalStateException("network interceptor " + interceptor
+ " must call proceed() exactly once");
}
// Confirm that the intercepted response isn't null.
if (response == null) {
throw new NullPointerException("interceptor " + interceptor + " returned null");
}
if (response.body() == null) {
throw new IllegalStateException(
"interceptor " + interceptor + " returned a response with no body");
}
return response;
}
1、先判斷是否超過list的size,如果超過則遍歷結(jié)束,如果沒有超過則繼續(xù)執(zhí)行
2、calls+1
3、new了一個RealInterceptorChain,其中然后下標(biāo)index+1
4、從list取出下一個interceptor對象
5、執(zhí)行interceptor的intercept方法
總結(jié)一下就是每一個RealInterceptorChain對應(yīng)一個interceptor,然后每一個interceptor再產(chǎn)生下一個RealInterceptorChain,直到List迭代完成。
攔截器
從上面的調(diào)用關(guān)系可以看出除了紅色圈出的攔截器之外都是系統(tǒng)提供的攔截器,這整個過程是遞歸的執(zhí)行過程,在 CallServerInterceptor 中得到最終的 Response 之后,將 response 按遞歸逐級進(jìn)行返回,期間會經(jīng)過 NetworkInterceptor 最后到達(dá) Application Interceptor 。
OkHttp是如何進(jìn)行數(shù)據(jù)緩存?
緩存策略
OkHttp使用了CacheInterceptor攔截器進(jìn)行數(shù)據(jù)緩存的控制使用了CacheStrategy實現(xiàn)了上面的流程圖,它根據(jù)之前緩存的結(jié)果與當(dāng)前將要發(fā)送Request的header進(jìn)行策略,并得出是否進(jìn)行請求的結(jié)果。根據(jù)輸出的networkRequest和cacheResponse的值是否為null給出不同的策略,如下:
| networkRequest | cacheResponse | result 結(jié)果 |
|---|---|---|
| null | null | only-if-cached (表明不進(jìn)行網(wǎng)絡(luò)請求,且緩存不存在或者過期,一定會返回503錯誤) |
| null | non-null | 不進(jìn)行網(wǎng)絡(luò)請求,直接返回緩存,不請求網(wǎng)絡(luò) |
| non-null | null | 需要進(jìn)行網(wǎng)絡(luò)請求,而且緩存不存在或者過去,直接訪問網(wǎng)絡(luò) |
| non-null | non-null | Header中包含ETag/Last-Modified標(biāo)簽,需要在滿足條件下請求,還是需要訪問網(wǎng)絡(luò) |
緩存算法
通過分析CacheInterceptor攔截器的intercept方法,我們可以發(fā)現(xiàn)具體的緩存都是使用了Cache類進(jìn)行,最后具體的實現(xiàn)在DiskLruCache類中。緩存實際上是一個比較復(fù)雜的邏輯,單獨的功能塊,實際上不屬于OKhttp上的功能,實際上是通過是http協(xié)議和DiskLruCache做了處理。LinkedHashMap可以實現(xiàn)LRU算法,并且在這個case里,它被用作對DiskCache的內(nèi)存索引
有興趣可以參考如下2篇文章的具體實現(xiàn):
OkHttp的連接池復(fù)用機制是怎么樣?
鏈路
RealConnection是Connection的實現(xiàn)類,代表著鏈接socket的鏈路,如果擁有了一個RealConnection就代表了我們已經(jīng)跟服務(wù)器有了一條通信鏈路,而且通過
RealConnection代表是連接socket鏈路,RealConnection對象意味著我們已經(jīng)跟服務(wù)端有了一條通信鏈路。
另外StreamAllocation類為流的橋梁,在RetryAndFollowUpInterceptor中進(jìn)行初始化,在ConnectInterceptor中進(jìn)行newStream操作,具體的連接攔截器代碼如下:
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, chain, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();
return realChain.proceed(request, streamAllocation, httpCodec, connection);
}
newStream創(chuàng)建留最后會調(diào)用到findConnection方法,這里面是連接復(fù)用的關(guān)鍵,如果再連接池中找到能復(fù)用的連接,則直接返回。
否則將RealConnection加入到鏈接池ConnectionPool中,具體代碼如下:
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
boolean foundPooledConnection = false;
RealConnection result = null;
Route selectedRoute = null;
Connection releasedConnection;
Socket toClose;
synchronized (connectionPool) {
if (released) throw new IllegalStateException("released");
if (codec != null) throw new IllegalStateException("codec != null");
if (canceled) throw new IOException("Canceled");
// Attempt to use an already-allocated connection. We need to be careful here because our
// already-allocated connection may have been restricted from creating new streams.
releasedConnection = this.connection;
toClose = releaseIfNoNewStreams();
if (this.connection != null) {
// We had an already-allocated connection and it's good.
result = this.connection;
releasedConnection = null;
}
if (!reportedAcquired) {
// If the connection was never reported acquired, don't report it as released!
releasedConnection = null;
}
if (result == null) {
// Attempt to get a connection from the pool.
Internal.instance.get(connectionPool, address, this, null);
if (connection != null) {
foundPooledConnection = true;
result = connection;
} else {
selectedRoute = route;
}
}
}
closeQuietly(toClose);
if (releasedConnection != null) {
eventListener.connectionReleased(call, releasedConnection);
}
if (foundPooledConnection) {
eventListener.connectionAcquired(call, result);
}
if (result != null) {
// If we found an already-allocated or pooled connection, we're done.
return result;
}
// If we need a route selection, make one. This is a blocking operation.
boolean newRouteSelection = false;
if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
newRouteSelection = true;
routeSelection = routeSelector.next();
}
synchronized (connectionPool) {
if (canceled) throw new IOException("Canceled");
if (newRouteSelection) {
// Now that we have a set of IP addresses, make another attempt at getting a connection from
// the pool. This could match due to connection coalescing.
List<Route> routes = routeSelection.getAll();
for (int i = 0, size = routes.size(); i < size; i++) {
Route route = routes.get(i);
Internal.instance.get(connectionPool, address, this, route);
if (connection != null) {
foundPooledConnection = true;
result = connection;
this.route = route;
break;
}
}
}
if (!foundPooledConnection) {
if (selectedRoute == null) {
selectedRoute = routeSelection.next();
}
// Create a connection and assign it to this allocation immediately. This makes it possible
// for an asynchronous cancel() to interrupt the handshake we're about to do.
route = selectedRoute;
refusedStreamCount = 0;
result = new RealConnection(connectionPool, selectedRoute);
acquire(result, false);
}
}
// If we found a pooled connection on the 2nd time around, we're done.
if (foundPooledConnection) {
eventListener.connectionAcquired(call, result);
return result;
}
// Do TCP + TLS handshakes. This is a blocking operation.
result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
connectionRetryEnabled, call, eventListener);
routeDatabase().connected(result.route());
Socket socket = null;
synchronized (connectionPool) {
reportedAcquired = true;
// Pool the connection.
Internal.instance.put(connectionPool, result);
// If another multiplexed connection to the same address was created concurrently, then
// release this connection and acquire that one.
if (result.isMultiplexed()) {
socket = Internal.instance.deduplicate(connectionPool, address, this);
result = connection;
}
}
closeQuietly(socket);
eventListener.connectionAcquired(call, result);
return result;
}
連接池
OkHttp中使用ConnectionPool管理http和http/2的鏈接,以便減少網(wǎng)絡(luò)請求延遲。同一個address將共享同一個connection。該類實現(xiàn)了復(fù)用連接的目標(biāo)。一個OkHttpClient只包含一個ConnectionPool,其實例化也是在OkHttpClient的過程。這里說一下ConnectionPool各個方法的調(diào)用并沒有直接對外暴露,而是通過OkHttpClient的Internal接口統(tǒng)一對外暴露。
1、獲取連接使用get方法,或獲取是否有合適的鏈接,否則返回null,具體實現(xiàn)如下:
RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
assert (Thread.holdsLock(this));
for (RealConnection connection : connections) {
if (connection.isEligible(address, route)) {
streamAllocation.acquire(connection, true);
return connection;
}
}
return null;
}
2、加入連接使用put方法,并且會是會觸發(fā)cleanupRunnable,清理連接。具體實現(xiàn)如下:
void put(RealConnection connection) {
assert (Thread.holdsLock(this));
if (!cleanupRunning) {
cleanupRunning = true;
executor.execute(cleanupRunnable);
}
connections.add(connection);
}
3、具體的連接回收機制,首先統(tǒng)計空閑連接數(shù)量,然后通過for循環(huán)查找最長空閑時間的連接以及對應(yīng)空閑時長,然后判斷是否超出最大空閑連接數(shù)(maxIdleConnections)或者或者超過最大空閑時間(keepAliveDurationNs),滿足其一則清除最長空閑時長的連接。如果不滿足清理條件,則返回一個對應(yīng)等待時間。具體的實現(xiàn)如下:
long cleanup(long now) {
int inUseConnectionCount = 0;
int idleConnectionCount = 0;
RealConnection longestIdleConnection = null;
long longestIdleDurationNs = Long.MIN_VALUE;
// Find either a connection to evict, or the time that the next eviction is due.
synchronized (this) {
for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
RealConnection connection = i.next();
// If the connection is in use, keep searching.
if (pruneAndGetAllocationCount(connection, now) > 0) {
inUseConnectionCount++;
continue;
}
idleConnectionCount++;
// If the connection is ready to be evicted, we're done.
long idleDurationNs = now - connection.idleAtNanos;
if (idleDurationNs > longestIdleDurationNs) {
longestIdleDurationNs = idleDurationNs;
longestIdleConnection = connection;
}
}
if (longestIdleDurationNs >= this.keepAliveDurationNs
|| idleConnectionCount > this.maxIdleConnections) {
// We've found a connection to evict. Remove it from the list, then close it below (outside
// of the synchronized block).
connections.remove(longestIdleConnection);
} else if (idleConnectionCount > 0) {
// A connection will be ready to evict soon.
return keepAliveDurationNs - longestIdleDurationNs;
} else if (inUseConnectionCount > 0) {
// All connections are in use. It'll be at least the keep alive duration 'til we run again.
return keepAliveDurationNs;
} else {
// No connections, idle or in use.
cleanupRunning = false;
return -1;
}
}
closeQuietly(longestIdleConnection.socket());
// Cleanup again immediately.
return 0;
}
OkHttp的底層網(wǎng)絡(luò)實現(xiàn)是什么?
1、OkHttp使用okio進(jìn)行io的操作。okio是由square公司開發(fā)的,它補充了java.io和java.nio的不足,以便能夠更加方便,快速的訪問、存儲和處理你的數(shù)據(jù)。OKHttp底層也是用該庫作為支持。而且okio使用起來很簡單,減少了很多io操作的基本代碼,并且對內(nèi)存和CPU使用做了優(yōu)化。
2、沒有依賴其他的關(guān)于Http實現(xiàn)的庫,底層使用了Socket,自己實現(xiàn)了Http1.X及2.X的協(xié)議。
OkHttp中代碼運用了那些設(shè)計模式,有什么巧妙的設(shè)計?
1、建造者模式
不管是OkHttpClient對象的創(chuàng)建還是Request對象、Respone對象,都使用了建造者模式,將復(fù)雜的對象創(chuàng)建統(tǒng)一在不同方法中,使得創(chuàng)建的過程更加簡單。
2、外觀模式
OkHttpClient對外提供了統(tǒng)一的調(diào)度,屏蔽了內(nèi)部的實現(xiàn),使得使用該網(wǎng)絡(luò)庫簡單便捷。
3、責(zé)任鏈模式
OkHttp中的攔截器使用了責(zé)任鏈模式,將不同的攔截器獨立實現(xiàn),動態(tài)組成鏈的調(diào)用形式。責(zé)任清晰,可動態(tài)擴(kuò)展。
為什么要用OkHttp?
目前Android開發(fā)中,主要的網(wǎng)絡(luò)框架有HttpClient、Volley、HttpURLConnection、OkHttp。
其中Android早就不推薦httpclient,5.0之后干脆廢棄,6.0刪除了HttpClient。所以HttpClient不考慮。Volley框架現(xiàn)在也已經(jīng)不再升級了,故目前考慮使用的有、HttpURLConnection及OkHttp。
相對HttpURLConnection,OkHttp使用更加便捷及靈活,且第三方社區(qū)活躍,相關(guān)資料齊全,成熟穩(wěn)定性高。OkHttp也得到了官方的認(rèn)可,并在不斷優(yōu)化更新,所以建議應(yīng)用優(yōu)先選擇OkHttp作為網(wǎng)絡(luò)框架。
總結(jié)
思考
在項目的開發(fā)過程中,我們經(jīng)常使用到大量的第三方框架,但可能知其然不知其所以然,通過不斷的思考反問為什么,從而去研究源碼中的實現(xiàn)。能讓我們對框架更加運用自如及解決一些底層疑難的問題。
參考資料
OKHTTP結(jié)合官網(wǎng)示例分析兩種自定義攔截器的區(qū)別
關(guān)于
歡迎關(guān)注我的個人公眾號
微信搜索:一碼一浮生,或者搜索公眾號ID:life2code