简介
OkOne是一款基于okhttp库的网络性能优化框架,但不同于其他框架对okhttp的使用调用进行封装,而是从不一样的方面,以对开发者无侵入的方式进行优化。
更多介绍见《OkOne-基于okhttp的网络性能优化框架》
GitHub地址
预建连
开发者可以在合适的时机提前建立连接,若连接成功,则会将其添加进okhttp连接池。
OkOne.preBuildConnection(okHttpClient, url, new PreConnectCallback() {
@Override
public void connectCompleted(String url) {
Log.d(TAG, "预建连成功: " + url);
}
@Override
public void connectFailed(Throwable t) {
Log.e(TAG, "预建连失败", t);
}
});
效果演示
- 首次尝试预建连
预连接 "https://stackoverflow.com/" ,连接成功。
- 重复预建连
尝试再次对 "https://stackoverflow.com/" 域名预建连,会直接返回错误,避免重复预建连。
- 接口请求
请求 "https://stackoverflow.com/" 响应成功,可以看到从callStart直接到connectionAcquired,省去了中间的DNS和握手建连过程。
原理剖析
OkHttp框架本身不对开发者开放预建连功能,要实现预建连功能必须了解OkHttp中的Connection(连接)创建和ConnectionPool(连接池)机制。接下来深入OkHttp框架源码中进行分析,找到实现预建连的插入点。
“源码基于OkHttp当前最新版本4.9.0
大家都知道OkHttp在发起请求后,会经过拦截链层层处理,其中ConnectInterceptor拦截器负责查找或新建Connection。
ConnectInterceptor#intercept -> RealCall#initExchange -> ExchangeFinder#find
ConnectInterceptor中是通过ExchangeFinder的find方法来获取Connection,直接来看ExchangeFinder#find方法。
fun find(
client: OkHttpClient,
chain: RealInterceptorChain
): ExchangeCodec {
try {
// 进一步获取可用Connection
val resultConnection = findHealthyConnection(
// ···限于篇幅省略参数代码
)
return resultConnection.newCodec(client, chain)
} catch (e: RouteException) {
// ···
} catch (e: IOException) {
// ···
}
}
接着看ExchangeFinder#findHealthyConnection:
@Throws(IOException::class)
private fun findHealthyConnection(
connectTimeout: Int,
readTimeout: Int,
writeTimeout: Int,
pingIntervalMillis: Int,
connectionRetryEnabled: Boolean,
doExtensiveHealthChecks: Boolean
): RealConnection {
while (true) {
// 进一步获取Connection
val candidate = findConnection(
// ···限于篇幅省略参数代码
)
// 检查Connection是否可用,若检查通过则返回该Connection。
// 否则,切换下一个Route用于尝试。
// 若再无可用Route,则抛异常退出。
// ···
}
}
关键看ExchangeFinder#findConnection方法:
@Throws(IOException::class)
private fun findConnection(
connectTimeout: Int,
readTimeout: Int,
writeTimeout: Int,
pingIntervalMillis: Int,
connectionRetryEnabled: Boolean
): RealConnection {
// ···
// 检查上一次请求的Connection是否可以复用
// ···
// 一些标记清零
// ···
// Attempt to get a connection from the pool.
// 检查连接池中是否存在相同地址的Connection
if (connectionPool.callAcquirePooledConnection(address, call, null, false)) {
val result = call.connection!!
eventListener.connectionAcquired(call, result)
return result
}
// Nothing in the pool. Figure out what route we'll try next.
val routes: List<Route>?
val route: Route
if (nextRouteToTry != null) {
// Use a route from a preceding coalesced connection.
// 使用来自先前合并连接的路由(连接成功后会检查连接池中是否有一样的连接,有则合并,并保存Route)。
// 初次连接不走这个case。
} else if (routeSelection != null && routeSelection!!.hasNext()) {
// Use a route from an existing route selection.
// 切换Route继续尝试时,初次连接不走这个case。
} else {
// Compute a new route selection. This is a blocking operation!
// 初次连接走这个case。
var localRouteSelector = routeSelector
if (localRouteSelector == null) {
localRouteSelector = RouteSelector(address, call.client.routeDatabase, call, eventListener)
this.routeSelector = localRouteSelector
}
val localRouteSelection = localRouteSelector.next()
routeSelection = localRouteSelection
// 获取一组Route。
routes = localRouteSelection.routes
if (call.isCanceled()) throw IOException("Canceled")
// Now that we have a set of IP addresses, make another attempt at getting a connection from
// the pool. We have a better chance of matching thanks to connection coalescing.
// 再次检查连接池。
if (connectionPool.callAcquirePooledConnection(address, call, routes, false)) {
val result = call.connection!!
eventListener.connectionAcquired(call, result)
return result
}
// 获取一个Route。
route = localRouteSelection.next()
}
// Connect. Tell the call about the connecting call so async cancels work.
// 新建Connection。
val newConnection = RealConnection(connectionPool, route)
call.connectionToCancel = newConnection
try {
// 尝试连接,若连接失败,方法内部会抛出异常
newConnection.connect(
connectTimeout,
readTimeout,
writeTimeout,
pingIntervalMillis,
connectionRetryEnabled,
call,
eventListener
)
} finally {
call.connectionToCancel = null
}
// 记录当前Route(从失败Route集合中移除该Route)。
call.client.routeDatabase.connected(newConnection.route())
// If we raced another call connecting to this host, coalesce the connections. This makes for 3
// different lookups in the connection pool!
// 再次检查连接池。
if (connectionPool.callAcquirePooledConnection(address, call, routes, true)) {
val result = call.connection!!
nextRouteToTry = route
newConnection.socket().closeQuietly()
eventListener.connectionAcquired(call, result)
return result
}
synchronized(newConnection) {
// 将新建的Connection添加进连接池(此方法也将触发连接池启动清理任务)。
connectionPool.put(newConnection)
call.acquireConnectionNoEvents(newConnection)
}
eventListener.connectionAcquired(call, newConnection)
// 返回新建的Connection。
return newConnection
}
在该方法中我们只关心新建Connection的关键步骤:
- 1.选取Route
- 2.新建RealConnection
- 3.connect进行建连
- 4.连接成功后加入ConnectionPool
一.如何获取Route?
通过分析前面findConnection方法,发现可以通过RouteSelector来获取。而创建RouteSelector又需要Address(作为构造方法的第一个参数),因此先来获取Address。
获取Address可以参考RealCall#createAddress方法:
private fun createAddress(url: HttpUrl): Address {
var sslSocketFactory: SSLSocketFactory? = null
var hostnameVerifier: HostnameVerifier? = null
var certificatePinner: CertificatePinner? = null
if (url.isHttps) {
sslSocketFactory = client.sslSocketFactory
hostnameVerifier = client.hostnameVerifier
certificatePinner = client.certificatePinner
}
return Address(
uriHost = url.host,
uriPort = url.port,
dns = client.dns,
socketFactory = client.socketFactory,
sslSocketFactory = sslSocketFactory,
hostnameVerifier = hostnameVerifier,
certificatePinner = certificatePinner,
proxyAuthenticator = client.proxyAuthenticator,
proxy = client.proxy,
protocols = client.protocols,
connectionSpecs = client.connectionSpecs,
proxySelector = client.proxySelector
)
}
构造RouteSelector的第二个参数RouteDatabase可以通过okHttpClient实例获取,第三个参数Call可以使用一个Call空实现,第四个参数EventListener可以使用EventListener.NONE。
有了RouteSelector便可以获取Route,预建连作为一个辅助优化功能,不强制必须成功,不必循环尝试所有Route,只需要通过next方法获取一次当前的Route。
拿到Address和Route后先通过RealConnectionPool#callAcquirePooledConnection方法检查一次连接池。RealConnectionPool可以通过反射从okHttpClient中获取。
在RealConnectionPool#callAcquirePooledConnection方法中会遍历连接池中的RealConnection,通过RealConnection#isMultiplexed和RealConnection#isEligible方法进行比较(除此之外还有其他操作)。预建连时只需要进行比较比较即可,因此参考该方法中的实现,自行通过反射来调用isMultiplexed、isEligible方法进行比较。
二.如何新建RealConnection?
使用前面获取的RealConnectionPool和Route直接new一个。
三.如何connect?
调用新建的RealConnection的connect方法。connect前四个参数通过okHttpClient获取。第五个参数connectionRetryEnabled传false,失败不重试,预建连不强求成功。第五、六个参数使用空实现。
connection.connect(
mClient.connectTimeoutMillis(),
mClient.readTimeoutMillis(),
mClient.writeTimeoutMillis(),
mClient.pingIntervalMillis(),
false,
BuildConnectionProcessor.NONE_CALL,
EventListener.NONE
);
若connect方法抛出异常,则视为预建连失败,不将其添加进连接池。连接成功后,再检查一次连接池,若已存在,则关闭当前新建的RealConnection的Socket。
四.如何添加进ConnectionPool?
调用RealConnectionPool的put方法把新建的RealConnection传入,同时会触发RealConnectionPool启动清理闲置RealConnection的任务(若未启动)。
synchronized (connection) {
realConnectionPool.put(connection);
}
至此完成预建连,完整实现见GitHub上源码。