一个示例
前后分别去请求同一个域名下的接口,通过 Charles 抓包,可以看到 Timing 下面的时间:
- 第二次请求时,DNS、Connect 和 TLS Handshake 部分都是
-
,说明没有这部分的耗时,对比第一次请求的这三个部分,节省了1 + 35 + 97
=133ms
。 - 当然第一次请求的 Request 和 Response 的 Size 比第二次要大一丢丢,且 Speed 低一些,忽略这些差异,在其他条件都一致的情况下,第二次请求比第一次请求能快 133ms。
第一次
第二次
这就是 http(s) 的连接复用。
连接复用
在此之前先简单复习一下发起网络 Request ->收到 Response 的粗略过程:
- 客户端发起网络请求
- 通过 DNS 服务解析域名,获取 IP 地址(一般是 UDP 协议)
- 建立 TCP 连接(三次握手)
- 建立 TLS 连接(Https)
- 发送网络请求 Request
- 服务器接收 Request 到后,执行逻辑并返回 Response
- 关闭 TCP 连接(四次挥手)
通过连接复用,上面的 2、3、4 步都不需要重新走了。使用 RTT 来定义这个时长,RTT(Round-Trip Time, 往返时间) 是网络请求从起点到目的地然后再回到起点所花费的时长。那么节省的时间是:
- DNS 一般使用 UDP 协议,最近重新复习了下 DNS 的内容,如果 DNS 响应报文的长度大于 512 字节,则会使用 TCP 协议。事实上,很多 DNS 服务器进行配置时,也仅支持 UDP。因此这一步可以看成节省了 1 个 RTT。
- 建立 TCP 连接,三次握手,需要 2 个 RTT。
- 建立 TLS 连接,根据 TLS 版本,有不同的 RTT。
HTTP1.1 版本开始默认就是持久连接,可以复用,通过在报文头部加上 Connection:Close
来关闭连接。另外空闲的持久连接也可以随时被关闭,即使不发送 Connection:Close
,也不意味着服务器连接永远保持打开。
预连接
常用的网络框架如 OkHttp 等,都支持 HTTP1.1 和 HTTP2 的功能,那也支持连接复用。
我们可以利用这个机制来做一个预连接,比如说在 APP 闪屏等待时,预先建立起关键页面域名的连接,这样在用户进入相应页面后可以更快的获取到网络请求结果,提升用户体验。
可以简单的对域名链接提前发起一个 HEAD 请求(没有Body),这样就能提前建立好连接,下次同域名的请求可以直接复用。
private val client by lazy { OkHttpClient() }
btn.setOnClickListener {
// 正式请求
launch(Dispatchers.IO) {
request()
}
}
// 预连接
launch(Dispatchers.IO) {
preRequest()
}
fun preRequest() {
val request = Request.Builder()
.head()
.url("xxx")
.build()
client.newCall(request).execute()
}
fun request() {
val request = Request.Builder()
.get()
.url("xxx/yyy")
.build()
client.newCall(request).execute()
}
可以抓包看到首次进入时发送的 head 请求和实际上点击发送的 get 请求:
预连接
正式请求
可以看到正式请求时,确实少了上述三个步骤的耗时。还可以分别看下 Connection 和 TLS 的信息:
预连接
正式请求
能看出来正式请求时,这俩是复用的(关注 TLS 的 Session Resumed
和 Connection 的 Server Connection
部分)。
另外 OkHttp 中有个 ConnectionPool 连接池,在使用 Connect 连接时,会优先复用已有的连接,无可用时才创建新连接。连接池里容纳的连接数是限定的(貌似默认是 5 个),如果业务比较复杂,请求比较多的话,可能会导致连接池占满,这样就会释放之前做好的预连接。因此一个比较简单的方式就是适当调大连接池的容量和超时时间。
总结
通过 http(s) 的连接复用机制,我们可以考虑使用预连接来优化 APP 中某些场景的网络请求速度,这需要我们根据实际业务场景以及服务器压力来判断是否进行预连接。
另外我们可以适当调大连接池的容量和超时时间,由于连接是双向的,即使客户端把 Connection 一直保留,服务端也会根据实际连接数量和时长来自动关闭连接的,所以调大连接池一般不会增大服务器压力。
预连接的效果实际和服务器配置有关,如果服务器把连接超时设置得很小,那每次请求可能都需要重新建立连接,这样客户端的预连接会失效,且服务器也需要不断创建和销毁 TCP 连接而浪费更多资源;如果服务器把连接超时设置得很大,那之前的连接长时间都不会释放,导致服务器服务的并发数受到影响,影响新的请求。因此调优需要多端协调,综合考虑。
为了帮助到大家更好的全面清晰的掌握好性能优化,准备了相关的核心笔记(还含底层逻辑):https://qr18.cn/FVlo89
性能优化核心笔记:https://qr18.cn/FVlo89
启动优化
内存优化
UI优化
网络优化
Bitmap优化与图片压缩优化 :https://qr18.cn/FVlo89
多线程并发优化与数据传输效率优化
体积包优化
《Android 性能监控框架》:https://qr18.cn/FVlo89
《Android Framework学习手册》:https://qr18.cn/AQpN4J
- 开机Init 进程
- 开机启动 Zygote 进程
- 开机启动 SystemServer 进程
- Binder 驱动
- AMS 的启动过程
- PMS 的启动过程
- Launcher 的启动过程
- Android 四大组件
- Android 系统服务 - Input 事件的分发过程
- Android 底层渲染 - 屏幕刷新机制源码分析
- Android 源码分析实战