前面一篇文章【OkHttp3 執行流程】里面分析了OkHttp3的請求過程。今天的文章將對OkHttp3的連接池的復用進行深一步的分析,通過對連接池的管理,復用連接,減少了頻繁的網絡請求導致性能下降的問題。我們知道,Http是基于TCP協議的,而TCP建立連接需要經過三次握手,斷開需要經過四次揮手,因此,Http中添加了一種KeepAlive機制,當數據傳輸完畢后仍然保持連接,等待下一次請求時直接復用該連接。
1、找到獲取連接的入口,ConnectInterceptor的intercept()方法
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");
// 1
HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();
return realChain.proceed(request, streamAllocation, httpCodec, connection);
}
在注釋1處利用StreamAllocation的newStream()獲取httpCodec對象,這個過程會從連接池中尋找是否有可用連接,若有,則返回;若沒有,則創建一個新的連接,并加入連接池中。
2、newStream()的內部實現
public HttpCodec newStream(
OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
int connectTimeout = chain.connectTimeoutMillis();
int readTimeout = chain.readTimeoutMillis();
int writeTimeout = chain.writeTimeoutMillis();
int pingIntervalMillis = client.pingIntervalMillis();
boolean connectionRetryEnabled = client.retryOnConnectionFailure();
try {
RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);
synchronized (connectionPool) {
codec = resultCodec;
return resultCodec;
}
} catch (IOException e) {
throw new RouteException(e);
}
}
在newStream()內部調用findHealthyConnection()尋找可用連接。
private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled,
boolean doExtensiveHealthChecks) throws IOException {
while (true) {
RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
pingIntervalMillis, connectionRetryEnabled);
// If this is a brand new connection, we can skip the extensive health checks.
synchronized (connectionPool) {
if (candidate.successCount == 0) {
return candidate;
}
}
// Do a (potentially slow) check to confirm that the pooled connection is still good. If it
// isn't, take it out of the pool and start again.
if (!candidate.isHealthy(doExtensiveHealthChecks)) {
noNewStreams();
continue;
}
return candidate;
}
}
輪詢的方式尋找,利用findConnection()尋找一個候選連接,先判斷是否為一個全新的連接,若是,跳過檢查,直接返回該連接;若不是,則檢查該連接是否依然可用。
3、findConnection()的內部實現
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");
//嘗試使用一個已分配的連接,但可能會限制我們創建新的流
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;
}
// 1. 試圖從連接池中獲取連接
if (result == null) {
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) {
// 如果上面從已分配或連接池其中一個能找到可用連接,則返回
return result;
}
... ...//省略代碼
//創建一個新的連接
result = new RealConnection(connectionPool, selectedRoute);
// 引用計數
acquire(result, false);
}
}
... ...//省略部分代碼
synchronized (connectionPool) {
reportedAcquired = true;
// 放入連接池
Internal.instance.put(connectionPool, result);
//如果另一個并發創建多路連接到相同的地址,則刪除重復數據
if (result.isMultiplexed()) {
socket = Internal.instance.deduplicate(connectionPool, address, this);
result = connection;
}
}
closeQuietly(socket);
eventListener.connectionAcquired(call, result);
return result;
}
上面代碼中有三個關鍵點,
1、Internal.instance.get() : 從連接池中獲取連接
2、acquire(): 引用計數,具體是對StreamAllocation的計數,通過aquire()與release()操作RealConnection中的List<Reference<StreamAllocation>>列表。
3、Internal.instance.put():將連接放入連接池中。
1和3中都有Internal.instance,instance實際就是Internal的一個實例,在創建OkHttpClient時已對instance進行了初始化,
4、初始化Internal的instance
Internal.instance = new Internal() {
... ...//省略了部分代碼
@Override
public RealConnection get(ConnectionPool pool, Address address,
StreamAllocation streamAllocation, Route route) {
return pool.get(address, streamAllocation, route);
}
//刪除重復數據
@Override
public Socket deduplicate(
ConnectionPool pool, Address address, StreamAllocation streamAllocation) {
return pool.deduplicate(address, streamAllocation);
}
@Override
public void put(ConnectionPool pool, RealConnection connection) {
pool.put(connection);
}
};
}
通過上面代碼發現,從連接池獲取可用連接和添加新的連接到連接池,實際調用的是ConnectionPool的get()和put()方法。
5、連接池管理ConnectionPool
在分析get和put操作前,先看下ConnectionPool的一些關鍵屬性
//線程池,核心線程數為0,最大線程數為最大整數,線程空閑存活時間60s,
//SynchronousQueue 直接提交策略
private static final Executor executor = new ThreadPoolExecutor(0,
Integer.MAX_VALUE , 60L , TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp ConnectionPool", true));
//空閑連接的最大連接數
private final int maxIdleConnections;
//保持連接的周期
private final long keepAliveDurationNs;
//雙端隊列,存放具體的連接
private final Deque<RealConnection> connections = new ArrayDeque<>();
//用于記錄連接失敗的route
final RouteDatabase routeDatabase = new RouteDatabase();
//構造函數
//從這里可以知道,空閑連接的最大連接數為5,保持連接的周期是5分鐘
public ConnectionPool() {
this(5, 5, TimeUnit.MINUTES);
}
public ConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) {
this.maxIdleConnections = maxIdleConnections;
this.keepAliveDurationNs = timeUnit.toNanos(keepAliveDuration);
// Put a floor on the keep alive duration, otherwise cleanup will spin loop.
if (keepAliveDuration <= 0) {
throw new IllegalArgumentException("keepAliveDuration <= 0: " + keepAliveDuration);
}
}
了解了ConnectionPool內部的一些關鍵屬性后,首先看下ConnectionPool 的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;
}
在get()方法內部對存放具體連接的雙端隊列connections進行遍歷,如果連接有效,則利用acquire()計數。
//StreamAllocation # 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;
//添加到RealConnection的allocations列表
connection.allocations.add(new StreamAllocationReference(this, callStackTrace));
}
//StreamAllocation # release()
private void release(RealConnection connection) {
for (int i = 0, size = connection.allocations.size(); i < size; i++) {
Reference<StreamAllocation> reference = connection.allocations.get(i);
if (reference.get() == this) {
//從RealConnection的allocations列表中移除
connection.allocations.remove(i);
return;
}
}
throw new IllegalStateException();
}
接著看ConnectionPool 的put()方法。
void put(RealConnection connection) {
assert (Thread.holdsLock(this));
if (!cleanupRunning) {
cleanupRunning = true;
executor.execute(cleanupRunnable);
}
connections.add(connection);
}
先判斷是否需要清理運行中的連接,然后添加新的連接到連接池。接下來看看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) {
try {
ConnectionPool.this.wait(waitMillis, (int) waitNanos);
} catch (InterruptedException ignored) {
}
}
}
}
}
};
在cleanupRunnable的run方法會不停的調用cleanup清理并返回下一次清理的時間間隔。然后進入wait,等待下一次的清理。那么cleanup()是怎么計算時間間隔的?
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();
//檢查連接是否是空閑狀態,
//不是,則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;
}
從上面可以知道,cleanupRunnable的主要工作是負責連接池的清理和回收。
總結: OkHttp3連接池的復用主要是對雙端隊列Deque<RealConnection>進行操作,通過對StreamAllocation的引用計數實現自動回收。