來吧,今天說說常用的網路框架OKHttp,也是現在Android所用的原生網路框架(Android 4.4
開始,HttpURLConnection
的底層實現被Google
改成了OkHttp
),GOGOGO!
- OKHttp有哪些攔截器,分別起什麼作用
- OkHttp怎麼實現連線池
- OkHttp裡面用到了什麼設計模式
OKHttp有哪些攔截器,分別起什麼作用
OKHTTP
的攔截器是把所有的攔截器放到一個list裡,然後每次依次執行攔截器,並且在每個攔截器分成三部分:
- 預處理攔截器內容
- 通過
proceed
方法把請求交給下一個攔截器 - 下一個攔截器處理完成並返回,後續處理工作。
這樣依次下去就形成了一個鏈式呼叫,看看原始碼,具體有哪些攔截器:
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);
}
根據原始碼可知,一共七個攔截器:
addInterceptor(Interceptor)
,這是由開發者設定的,會按照開發者的要求,在所有的攔截器處理之前進行最早的攔截處理,比如一些公共引數,Header都可以在這裡新增。RetryAndFollowUpInterceptor
,這裡會對連線做一些初始化工作,以及請求失敗的充實工作,重定向的後續請求工作。跟他的名字一樣,就是做重試工作還有一些連線跟蹤工作。BridgeInterceptor
,這裡會為使用者構建一個能夠進行網路訪問的請求,同時後續工作將網路請求回來的響應Response轉化為使用者可用的Response,比如新增檔案型別,content-length計算新增,gzip解包。CacheInterceptor
,這裡主要是處理cache相關處理,會根據OkHttpClient物件的配置以及快取策略對請求值進行快取,而且如果本地有了可⽤的Cache,就可以在沒有網路互動的情況下就返回快取結果。ConnectInterceptor
,這裡主要就是負責建立連線了,會建立TCP連線或者TLS連線,以及負責編碼解碼的HttpCodecnetworkInterceptors
,這裡也是開發者自己設定的,所以本質上和第一個攔截器差不多,但是由於位置不同,所以用處也不同。這個位置新增的攔截器可以看到請求和響應的資料了,所以可以做一些網路除錯。CallServerInterceptor
,這裡就是進行網路資料的請求和響應了,也就是實際的網路I/O操作,通過socket讀寫資料。
OkHttp怎麼實現連線池
- 為什麼需要連線池?
頻繁的進行建立Sokcet
連線和斷開Socket
是非常消耗網路資源和浪費時間的,所以HTTP中的keepalive
連線對於降低延遲和提升速度有非常重要的作用。keepalive機制
是什麼呢?也就是可以在一次TCP連線中可以持續傳送多份資料而不會斷開連線。所以連線的多次使用,也就是複用就變得格外重要了,而複用連線就需要對連線進行管理,於是就有了連線池的概念。
OkHttp中使用ConectionPool
實現連線池,預設支援5個併發KeepAlive
,預設鏈路生命為5分鐘。
- 怎麼實現的?
1)首先,ConectionPool
中維護了一個雙端佇列Deque
,也就是兩端都可以進出的佇列,用來儲存連線。
2)然後在ConnectInterceptor
,也就是負責建立連線的攔截器中,首先會找可用連線,也就是從連線池中去獲取連線,具體的就是會呼叫到ConectionPool
的get方法。
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;
}
也就是遍歷了雙端佇列,如果連線有效,就會呼叫acquire方法計數並返回這個連線。
3)如果沒找到可用連線,就會建立新連線,並會把這個建立的連線加入到雙端佇列中,同時開始執行執行緒池中的執行緒,其實就是呼叫了ConectionPool
的put方法。
public final class ConnectionPool {
void put(RealConnection connection) {
if (!cleanupRunning) {
//沒有連線的時候呼叫
cleanupRunning = true;
executor.execute(cleanupRunnable);
}
connections.add(connection);
}
}
3)其實這個執行緒池中只有一個執行緒,是用來清理連線的,也就是上述的cleanupRunnable
private final Runnable cleanupRunnable = new Runnable() {
@Override
public void run() {
while (true) {
//執行清理,並返回下次需要清理的時間。
long waitNanos = cleanup(System.nanoTime());
if (waitNanos == -1) return;
if (waitNanos > 0) {
long waitMillis = waitNanos / 1000000L;
waitNanos -= (waitMillis * 1000000L);
synchronized (ConnectionPool.this) {
//在timeout時間內釋放鎖
try {
ConnectionPool.this.wait(waitMillis, (int) waitNanos);
} catch (InterruptedException ignored) {
}
}
}
}
}
};
這個runnable
會不停的呼叫cleanup方法清理執行緒池,並返回下一次清理的時間間隔,然後進入wait等待。
怎麼清理的呢?看看原始碼:
long cleanup(long now) {
synchronized (this) {
//遍歷連線
for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
RealConnection connection = i.next();
//檢查連線是否是空閒狀態,
//不是,則inUseConnectionCount + 1
//是 ,則idleConnectionCount + 1
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;
}
}
//如果超過keepAliveDurationNs或maxIdleConnections,
//從雙端佇列connections中移除
if (longestIdleDurationNs >= this.keepAliveDurationNs
|| idleConnectionCount > this.maxIdleConnections) {
connections.remove(longestIdleConnection);
} else if (idleConnectionCount > 0) { //如果空閒連線次數>0,返回將要到期的時間
// A connection will be ready to evict soon.
return keepAliveDurationNs - longestIdleDurationNs;
} else if (inUseConnectionCount > 0) {
// 連線依然在使用中,返回保持連線的週期5分鐘
return keepAliveDurationNs;
} else {
// No connections, idle or in use.
cleanupRunning = false;
return -1;
}
}
closeQuietly(longestIdleConnection.socket());
// Cleanup again immediately.
return 0;
}
也就是當如果空閒連線maxIdleConnections
超過5個或者keepalive時間大於5分鐘,則將該連線清理掉。
4)這裡有個問題,怎樣屬於空閒連線?
其實就是有關剛才說到的一個方法acquire
計數方法:
public void acquire(RealConnection connection, boolean reportedAcquired) {
assert (Thread.holdsLock(connectionPool));
if (this.connection != null) throw new IllegalStateException();
this.connection = connection;
this.reportedAcquired = reportedAcquired;
connection.allocations.add(new StreamAllocationReference(this, callStackTrace));
}
在RealConnection
中,有一個StreamAllocation
虛引用列表allocations
。每建立一個連線,就會把連線對應的StreamAllocationReference
新增進該列表中,如果連線關閉以後就將該物件移除。
5)連線池的工作就這麼多,並不負責,主要就是管理雙端佇列Deque<RealConnection>
,可以用的連線就直接用,然後定期清理連線,同時通過對StreamAllocation
的引用計數實現自動回收。
OkHttp裡面用到了什麼設計模式
- 責任鏈模式
這個不要太明顯,可以說是okhttp的精髓所在了,主要體現就是攔截器的使用,具體程式碼可以看看上述的攔截器介紹。
- 建造者模式
在Okhttp中,建造者模式也是用的挺多的,主要用處是將物件的建立與表示相分離,用Builder組裝各項配置。
比如Request:
public class Request {
public static class Builder {
@Nullable HttpUrl url;
String method;
Headers.Builder headers;
@Nullable RequestBody body;
public Request build() {
return new Request(this);
}
}
}
- 工廠模式
工廠模式和建造者模式類似,區別就在於工廠模式側重點在於物件的生成過程,而建造者模式主要是側重物件的各個引數配置。
例子有CacheInterceptor攔截器中又個CacheStrategy物件:
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
public Factory(long nowMillis, Request request, Response cacheResponse) {
this.nowMillis = nowMillis;
this.request = request;
this.cacheResponse = cacheResponse;
if (cacheResponse != null) {
this.sentRequestMillis = cacheResponse.sentRequestAtMillis();
this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis();
Headers headers = cacheResponse.headers();
for (int i = 0, size = headers.size(); i < size; i++) {
String fieldName = headers.name(i);
String value = headers.value(i);
if ("Date".equalsIgnoreCase(fieldName)) {
servedDate = HttpDate.parse(value);
servedDateString = value;
} else if ("Expires".equalsIgnoreCase(fieldName)) {
expires = HttpDate.parse(value);
} else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
lastModified = HttpDate.parse(value);
lastModifiedString = value;
} else if ("ETag".equalsIgnoreCase(fieldName)) {
etag = value;
} else if ("Age".equalsIgnoreCase(fieldName)) {
ageSeconds = HttpHeaders.parseSeconds(value, -1);
}
}
}
}
- 觀察者模式
之前我寫過一篇文章,是關於Okhttp中websocket的使用,由於webSocket屬於長連線,所以需要進行監聽,這裡是用到了觀察者模式:
final WebSocketListener listener;
@Override public void onReadMessage(String text) throws IOException {
listener.onMessage(this, text);
}
- 單例模式
這個就不舉例了,每個專案都會有
- 另外有的部落格還說到了策略模式,門面模式等,這些大家可以網上搜搜,畢竟每個人的想法看法都會不同,細心找找可能就會發現。
拜拜
有一起學習的小夥伴可以關注下❤️我的公眾號——碼上積木,每天剖析一個知識點,我們一起積累知識。