前面一篇文章【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
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(), Util.threadFactory("OkHttp ConnectionPool", true));
//空闲连接的最大连接数
private final int maxIdleConnections;
//保持连接的周期
private final long keepAliveDurationNs;
//双端队列,存放具体的连接
private final Deque 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 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 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