推荐一个基于 okhttp 的网络性能优化库
转自:dehang0
https://juejin.cn/post/6908178914779561997
本来以为这是一个 okhttp 的封装库,详细看了下,才了解是一个基于 okhttp 的,提升网络连接性能的框架。
我这边把作者两篇文章合并成了一篇,前部分说明用法,后部分将一些核心原理。
简介
OkOne是一款基于okhttp库的网络性能优化框架,但不同于其他框架对okhttp的使用调用进行封装,而是从不一样的方面,以对开发者无侵入的方式进行优化。
痛点
在APP项目中可能会包含多个组件模块,或依赖多个三方库,甚至部门分不同团队开发各自业务模块AAR供APP集成。其中可能都有使用到okhttp框架进行网络请求,不同的组件模块和三方库中各自创建OkHttpClient实例,或有开发者未通过单例缓存OkHttpClient,而是每次请求每次新建。这样将造成极大浪费,并且导致不能充分利用okhttp的请求队列和连接池等控制和优化措施。
解决
借助该OkOne库可以无侵入地将分散在不同组件中的OkHttpClient进行收敛,由OkOne进行统一管理和复用。OkOne会比较OkHttpClient.Builder进行区分复用,即相同配置的OkHttpClient.Builder将自动复用同一个OkHttpClient实例。
集成
集成很简单,仅需三步:
Minimum supported Gradle version is 6.5
1.在项目根目录的build.gradle里添加依赖
dependencies { classpath 'com.cdh.okone:gradle:0.1.0'}
2.在app module的build.gradle里应用插件
apply plugin: 'plugin.cdh.okone'
3.在app module的build.gradle的dependencies里添加依赖
implementation 'com.cdh.okone:okone:0.1.2'
至此已完成接入,运行即会自动生效。
效果
现在来看看实际效果,在demo中创建三个不同配置的OkHttpClient.Builder:
// builder1OkHttpClient.Builder builder1 = new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .addInterceptor(new HttpLoggingInterceptor()) .eventListener(mEventListener);// builder2OkHttpClient.Builder builder2 = new OkHttpClient.Builder() .retryOnConnectionFailure(true) .minWebSocketMessageToCompress(2048) .eventListener(mEventListener); testRequestServer(builder2);// builder3OkHttpClient.Builder builder3 = new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .addInterceptor(new HttpLoggingInterceptor()) .retryOnConnectionFailure(true) .minWebSocketMessageToCompress(2048) .eventListener(mEventListener);
实例复用
接下来分别用这三个Builder构建OkHttpClient进行请求:
private void testRequestServer(OkHttpClient.Builder builder) { // 这里不缓存client,每次都build OkHttpClient client = builder.build(); // 打印日志 Log.d(TAG, "创建OkHttpClient: " + client); Request request = new Request.Builder() .url(api) .build(); Call call = client.newCall(request); call.enqueue(new Callback() { @Override public void onFailure(@NotNull Call call, @NotNull IOException e) {} @Override public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {} });}
可以看到这里每次都build一个OkHttpClient来进行请求。
现在按照下面的顺序来调用:
// 先使用builder1请求两次testRequestServer(newBuilder1());testRequestServer(newBuilder1());// 换成builder2请求两次testRequestServer(newBuilder2());testRequestServer(newBuilder2());// 再换成builder3请求两次testRequestServer(newBuilder3());testRequestServer(newBuilder3());
接着来看看打印日志:
可以看到OkOne成功对OkHttpClient实例进行了复用,虽然每次请求都build来获得OkHttpClient,但不会实际产生多个实例。并且不同配置的Builder不会互相影响,通过builder1、builder2、builder3构建分别复用各自的OkHttpClient。如果未集成OkOne,那么将会产生6个OkHttpClient实例。
连接复用
继续看连接是否成功复用,通过EventListener添加日志来查看:
private EventListener mEventListener = new EventListener() { // 在每个重写方法中添加Log打印日志 // 限于篇幅省略代码···}
通过OkHttpClient.Builder#eventListener设置自定义EventListener。
接下来仍然按照Builder1、Builder1、Builder2、Builder2、Builder3、Builder3的顺序请求,查看日志。
Builder1第一次请求
可以看到当前还没有可复用连接,请求经历了dns和握手建连过程。
Builder1第二次请求
此次请求有复用连接,免去了dns和握手建连的过程。若未集成OkOne,则还会经历一次完整的建连过程。
Builder2第一次请求
builder2和builder1配置不同,不复用builder1的OkHttpClient,因此走完整请求过程。
Builder2第二次请求
此时请求也不用再dns和握手建连。
Builder3第一次请求
新建OkHttpClient,无复用连接。
Builder3第二次请求
成功复用。
可以看到有效利用了okhttp的连接池,避免每次请求都重新走dns和握手建连过程。若未集成OkOne库则每次都走完整请求过程。
更多功能
关闭开关
是否启用或关闭OkHttpClient统一复用和管理,需要在创建OkHttpClient前设置。
OkOne.useGlobalClient = true;
打印日志
打开或关闭OkOne打印日志。
OkOne.setLogEnable(true);
单独创建不受控的OkHttpClient实例
单独创建一个不经OkOne管理和复用的OkHttpClient。
OkHttpClient client = new OkHttpClient(builder);
GitHub地址
https://github.com/chidehang/OkOne
高级功能之OkHttp预建连以及原理剖析
预建连
开发者可以在合适的时机提前建立连接,若连接成功,则会将其添加进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/" 响应成功,可以看到从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上源码。https://github.com/chidehang/OkOne
- EOF -
PS:如果觉得我的分享不错,欢迎大家随手点赞、在看。
大家一起在评论区聊聊呗~