系列文章目录
1. Android 网络性能优化(1)概述 2. Android 网络性能优化(2)DNS优化 3. Android 网络性能优化(3)复用连接池 4. Android 网络性能优化(4)弱网优化
1. 概述
复用连接池是一个优化连接的技术,在Android中,功能强大的OkHttp
已经帮我们实现了这个技术,使我们不用再担心多请求时的性能低下。虽然已经帮咱实现了,但是我们可以学习学习(所以该篇比较简单),顺便可以重温下OkHttp的源码。
该篇承接 Android 网络性能优化(2)DNS优化,在拿到服务器ip后,我们客户端和服务端需要建立Socket,走Tcp的三次握手,在请求完成后通过四次分手关闭Socket。下图为三次握手流程:
如果程序产生了频繁的、数量较多的 网络请求,大量的连接每次都要握手和分手,必然会造成性能低下。Http有一种叫做keep-alive connections
的机制,在我们Http的请求中,会看到有下面个meta-daya:
它的作用是可以在传输数据后仍然保持连接,当客户端需要再次获取数据时,直接使用刚刚空闲下来的连接而不需要再次握手。
这也是比较常规的实现长连接的做法。
在Http1.0中,该选项默认为False,而在Http1.1中,Keep-Alive 是默认开启的。OkHttp默认实现5个并发的Keepalive connection,默认的连接keep时长为5分钟。
2. 连接池
连接池,就是请求通过复用存在的连接,达到节省开辟新连接所需开销的结果。这也是一种设计模式,是一种浅学设计模式之享元模式(19/23)。
因为连接的场景有多种(Spdy、SSL、WebSocket等),所以Socket的种类也有多种,连接池的分类可以参照下图:
上图展示了连接池的多种不同类型,可以看到最根基的的TCPSocket
连接,分别来看下每个连接池的作用:
- SSL连接池
管理SSLSocket,但SSLSocket又依赖于TCP连接池提供的TCPSocket - HTTP代理连接池
如果走HTTP协议,那么就需要TCP连接池提供TCPSocket,如果走HTTPS协议,那么就需要SSL连接池提供SSLSocket; - SpdySession池
依赖SSL连接池提供SSLSocket,这里需要说明下,虽然HTTP/2协议没有强制绑定HTTPS,但是在实际开发中确实都是绑定HTTPS - SOCKS连接池
管理的SOCKSSocket和SOCKS5Socket都需要依赖TCP连接池提供的TCPSocket - WebSocket连接池
依赖TCP连接池提供的TCPSocket,声明下这里没有说明WSS(Web Socket Secure)的情况
3. 源码实现
这里参考的是OkHttp4的代码
3.1 ConnectionPool类
连接池的类位于okhttp3.ConnectionPool
。我们需要了解到如何在timeout时间内复用connection,并且有效的对其进行回收清理操作。我们先来看看该类的作用,因为有文档注释,我们来看看官方是如何描述该类的
翻译:该类管理 Http/Http2 的连接复用,用来减少网络的消耗。有着相同ip地址的Http请求可以共享一个连接通道。该类实现了一种长连接的策略。
构造函数创造了一个新的连接池和附带参数,这些参数可能会在未来的OkHttp版本中被更改(也就是说不建议我们直接使用)。目前这个连接池可以最多同时持有5个闲置的连接,如果有多的连接,将会被移除掉。
// ConnectionPool.kt
class ConnectionPool internal constructor(
internal val delegate: RealConnectionPool
) {
constructor(
maxIdleConnections: Int,
keepAliveDuration: Long,
timeUnit: TimeUnit
) : this(RealConnectionPool(
taskRunner = TaskRunner.INSTANCE,
maxIdleConnections = maxIdleConnections,
keepAliveDuration = keepAliveDuration,
timeUnit = timeUnit
))
// 1
constructor() : this(5, 5, TimeUnit.MINUTES)
..
}
这是构造函数,注释1中可以看出,默认就的最多闲置连接是5个,保持时间是5分钟,taskRunner
是一个线程管理器,用来检测闲置socket并对其进行清理,在3.x版本中,它是一个Executor
。然后这个类就没别的东西了,其他的都在它的父类RealConnectionPool
里面了
3.2 RealConnectionPool的缓存操作
RealConnectionPool
是真正的连接池,ConnectionPool
是其子类,他除了刚刚那几个子类传来的参数之外,还有一个很重要的参数:
/**
* 使用线程安全的双向队列来管理所有的 [RealConnection]---Socket连接
*/
private val connections = ConcurrentLinkedQueue<RealConnection>()
连接池可以通过 connections
来管理连接的添加、删除、复用。
3.2.1 put操作
fun put(connection: RealConnection) {
connection.assertThreadHoldsLock()
// 1
connections.add(connection)
// 2
cleanupQueue.schedule(cleanupTask)
}
注释1: 在连接池connections中添加一个连接。
注释2: 需要整理一遍connections里的连接,比如说多出来的连接需要删除掉,超过保持时长的连接要去掉。
3.2.2 判断连接是否可以复用
在3.x版本,该类提供了一个方法来返回一个可复用的连接,主要逻辑是遍历connections的所有连接,判断是否有连接可复用。而4.x的版本稍微的更改逻辑,先来看下这个方法:
// 1
fun callAcquirePooledConnection(
address: Address,
call: RealCall,
routes: List<Route>?,
requireMultiplexed: Boolean
): Boolean {
for (connection in connections) {
synchronized(connection) {
// 2
if (requireMultiplexed && !connection.isMultiplexed) return@synchronized
// 3
if (!connection.isEligible(address, routes)) return@synchronized
// 4
call.acquireConnectionNoEvents(connection)
return true
}
}
return false
}
注释1: 传入一个ip地址,该方法就是判断是否已经存在该ip打通的socket,如果有返回true,说明可以复用,否则返回false
注释2: 判断连接的多路复用,这个属性是给Http2用的
注释3: 检查ip地址和路由列表是否合法
注释4: 调用 RealCall.acquireConnectionNoEvents()
方法,将RealCall的 connection指向该连接,表明存在可以复用的连接,并且返回true。那么调用者就可以通过它的RealCall来获取到复用的连接了。
可以看下 RealCall的方法:
// RealCall.kt
fun acquireConnectionNoEvents(connection: RealConnection) {
connection.assertThreadHoldsLock()
check(this.connection == null)
this.connection = connection
connection.calls.add(CallReference(this, callStackTrace))
}
3.2.3 清除和回收连接
在刚刚put方法里面,我们看到了该类会实现一个方法来check连接池里的连接,它的作用是清除和回收超时和多出来的连接,我们来看看这个方法,因为方法比较长,所以分成两个部分来看,下面是上半部分:
// RealConnectionPool.kt
/**
* 作用是维护连接池,删除那些超时的连接、或者超出最大数量限制的连接
* 返回的值是睡眠到下次执行该方法的时间,
* 如果不需要进一步清理,则返回-1
*/
fun cleanup(now: Long): Long {
var inUseConnectionCount = 0
var idleConnectionCount = 0
var longestIdleConnection: RealConnection? = null
var longestIdleDurationNs = Long.MIN_VALUE
// 1
for (connection in connections) {
synchronized(connection) {
// 2
if (pruneAndGetAllocationCount(connection, now) > 0) {
inUseConnectionCount++
} else {
idleConnectionCount++
// 3
val idleDurationNs = now - connection.idleAtNs
if (idleDurationNs > longestIdleDurationNs) {
longestIdleDurationNs = idleDurationNs
longestIdleConnection = connection
} else {
Unit
}
}
}
}
...
}
注释1: 遍历连接池内的所有连接
注释2: 调用 pruneAndGetAllocationCount()
方法,查看该连接是否正在被使用。如果正在使用,则工作连接+1
,否则 闲置连接+1
注释3: 计算该连接的闲置时间。遍历一圈,记录下闲置时间最久的连接。
再来看下cleanup()
的下半部分:
// RealConnectionPool.kt
....
when {
// 1
longestIdleDurationNs >= this.keepAliveDurationNs
|| idleConnectionCount > this.maxIdleConnections -> {
val connection = longestIdleConnection!!
synchronized(connection) {
if (connection.calls.isNotEmpty()) return 0L // No longer idle.
if (connection.idleAtNs + longestIdleDurationNs != now) return 0L // No longer oldest.
connection.noNewExchanges = true
(longestIdleConnection)
}
connection.socket().closeQuietly()
if (connections.isEmpty()) cleanupQueue.cancelAll()
// Clean up again immediately.
return 0L
}
// 2
idleConnectionCount > 0 -> {
return keepAliveDurationNs - longestIdleDurationNs
}
// 3
inUseConnectionCount > 0 -> {
return keepAliveDurationNs
}
// 4
else -> {
return -1
}
}
这里是根据上半部分的统计结果进行处理:
注释1:闲置最久的连接时间已经超过5分钟或者当前空闲的连接数超过了5个,则通过 connections.remove()
和 connection.socket().closeQuietly()
移除掉闲置最久的连接,
注释2:当前存在闲置连接,则返回 闲置最久的连接还需要等待多少时间就到5分钟 的时间间隔
注释3:当前没有闲置连接,有工作连接, 则返回 5分钟
注释4:既没有工作连接又没有闲置连接,返回-1
这个方法主要就是通过计算有无超时的限制连接或则超过容量的连接进行删除,其中它使用了一个方法 pruneAndGetAllocationCount()
来查看一个连接是否正在被使用,我们可以看看这个方法的逻辑。
3.2.4 查看连接是否闲置
// RealConnectionPool.kt
/**
* 删除所有的发生泄漏的回调,然后返回[Connection]剩余的实时的被调用的数量
*
* 如果一个回调正在被引用但是实际上已经被代码不使用他们了,这个回调就是泄漏的,
* 这种泄漏检测是不靠谱的,而且依赖于 GC回收
*/
private fun pruneAndGetAllocationCount(connection: RealConnection, now: Long): Int {
connection.assertThreadHoldsLock()
// 1
val references = connection.calls
var i = 0
// 2
while (i < references.size) {
val reference = references[i]
// 3
if (reference.get() != null) {
i++
continue
}
// 4
val callReference = reference as CallReference
val message = "A connection to ${connection.route().address.url} +
"Did you forget to close a response body?"
Platform.get().logCloseableLeak(message, callReference.callStackTrace)
// 5
references.removeAt(i)
connection.noNewExchanges = true
if (references.isEmpty()) {
connection.idleAtNs = now - keepAliveDurationNs
return 0
}
}
return references.size
}
注释1:获取 connection的所有存储的 Reference<RealCall>
,也就是Call引用
注释2:遍历这些Call
注释3:如果Call通过 .get()
获取不为null,那么说明它正在被使用,则记录并continue,否则说明这个call已经被清除了,但是由于在列表中所有没有回收掉。
注释4: 抛出一个泄漏的Log,提醒开发者有没有遗漏close一个连接。(所以这里提示我们在使用完一个socket后,需要关闭到RealCall,否则我们就复用不了连接池)
注释5:Call列表移除掉这个泄漏的Call。
总的来说,pruneAndGetAllocationCount()
这个方法就是通过检查 Reference
来查看引用数,判断一个连接是否被call引用,如果引用,就说明这个连接是一个正在工作中的连接,否则就是一个闲置的连接。
3.3 OkHttp中的使用
我们已经知道 RealConnectionPool
、ConnectionPool()
是如何复用连接池了,那么我们来看看它是在什么时候运用在代码中的吧。
在 OkHttp的大管家 OkHttpClient
的构造方法中,就发现了连接池的实例化:
// OkHttpClient
class Builder constructor() {
internal var dispatcher: Dispatcher = Dispatcher()
internal var connectionPool: ConnectionPool = ConnectionPool()
....
所以我们平时使用OkHttp的时候,就默认使用了这个 ConnectionPool了。
4. 总结
复用连接池可以减少多个网络请求下的连接建立的消耗,而且OkHttp已经默认帮我们实现了这些功能,我们只需要注意几点:
- 复用连接的前提是同ip通道,如果每个请求都发送给不同的ip,那么连接池也复用不了
- 最好养成习惯,在非Http2的使用完连接后(就是Http请求完成后),我们需要手动 关闭
RealCall
,否则下层代码就要通过触发GC回收来帮我们检查和关闭。
5. 参考文章
Android网络编程(八)源码解析OkHttp后篇[复用连接池]百度APP移动端网络深度优化实践分享(二):网络连接优化篇okhttp连接池复用机制