推荐一个基于 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:如果觉得我的分享不错,欢迎大家随手点赞、在看。

大家一起在评论区聊聊呗~

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值