首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Android网络全链路拆解:一次HTTP请求背后的性能陷阱

Android网络全链路拆解:一次HTTP请求背后的性能陷阱

作者头像
陆业聪
发布2026-05-11 14:44:04
发布2026-05-11 14:44:04
2620
举报

📚 Android网络优化系列 · 第1/5篇

从DNS到连接池,打造极速网络体验

👉 第1篇:Android网络全链路拆解:一次HTTP请求背后的性能陷阱(本篇)

⏳ 第2篇:DNS优化实战:从运营商DNS到HttpDNS的进化之路

⏳ 第3篇:连接优化与复用:让每一次握手都物超所值

⏳ 第4篇:数据压缩与缓存策略:把带宽用到极致

⏳ 第5篇:网络监控与容灾:让网络问题无处遁形

📰 科技要闻

• OkHttp 5.3.2 发布,持续优化连接池管理和HTTP/2多路复用——恰好是本系列的核心话题,后续篇章会深入拆解这些改进。

• Retrofit 3.0.0 正式版落地,升级至OkHttp 4.12 Kotlin版本,transitive Kotlin依赖让项目依赖树更清爽。

• Android官方更新网络连接文档(2026-04-25),新增DNS解析、HTTP客户端选项和Repository模式最佳实践。

📋 系列总览

这个系列一共5篇,目标是把Android网络优化从底层到监控完整拆一遍:

第1篇(本篇):全链路拆解——搞清楚一次HTTP请求经历了什么,性能瓶颈在哪

第2篇:DNS优化——运营商DNS到HttpDNS,解决劫持和调度问题

第3篇:连接优化与复用——TCP/TLS握手成本最小化,HTTP/2和连接池实战

第4篇:数据压缩与缓存——协议层压缩、业务层缓存、离线策略

第5篇:网络监控与容灾——埋点体系搭建、弱网降级、灾备切换

每篇都有可跑的代码,不讲概念空话。让我们开始。

从一个线上故障说起

上个月我们收到一批用户反馈:App在某些场景下"卡白屏"——准确说是首屏接口迟迟没有返回。查日志发现,同一个接口在WiFi下200ms搞定,切到4G弱信号环境直接飙到6-8秒,最夸张的case等了15秒才拿到数据。

第一反应是服务端慢了。但服务端的access log显示处理时间只有50ms——耗时几乎全在链路上。

这里有个很多人忽略的事实:一次HTTP请求的耗时,大部分不在服务端。尤其在移动端,网络链路本身的开销往往占总时间的80%以上。你觉得是服务端慢,其实是DNS解析花了2秒,是TLS握手重来了一次,是TCP连接在弱网下反复重传。

今天就来完整拆一下,一次HTTP请求到底经历了什么,每个环节的性能陷阱在哪,以及OkHttp/Retrofit架构下我们能从哪些地方下手优化。

一次HTTP请求的完整链路

当你调用 retrofit.create(ApiService::class.java).getData() 的那一刻,底层实际上要跑完这样一条链路:

DNS解析:把域名翻译成IP地址(0-2000ms,取决于缓存命中情况)

TCP三次握手:和服务器建立可靠连接(1个RTT,约50-300ms)

TLS握手:HTTPS加密协商(1-2个RTT,约100-500ms)

HTTP请求发送:发请求头+请求体(取决于body大小)

服务端处理:后端逻辑执行(通常最快的环节,讽刺不?)

HTTP响应接收:收响应头+响应体(取决于数据量和带宽)

数据解析:JSON/Proto反序列化(CPU密集,通常10-100ms)

用OkHttp的EventListener可以精确测量每个阶段的耗时。这是我们线上实际用的监控代码:

代码语言:javascript
复制
class NetworkTimingListener : EventListener() {private var callStartMs = 0L
private var dnsStartMs = 0L
private var connectStartMs = 0L
private var tlsStartMs = 0L
private var requestStartMs = 0L
private var responseStartMs = 0Loverride fun callStart(call: Call) {
callStartMs = System.currentTimeMillis()
}override fun dnsStart(call: Call, domainName: String) {
dnsStartMs = System.currentTimeMillis()
}override fun dnsEnd(call: Call, domainName: String, inetAddressList: List<InetAddress>) {
val dnsMs = System.currentTimeMillis() - dnsStartMs
reportMetric("dns_time", dnsMs)
}override fun connectStart(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy) {
connectStartMs = System.currentTimeMillis()
}override fun secureConnectStart(call: Call) {
tlsStartMs = System.currentTimeMillis()
}override fun secureConnectEnd(call: Call, handshake: Handshake?) {
val tlsMs = System.currentTimeMillis() - tlsStartMs
reportMetric("tls_time", tlsMs)
}override fun connectEnd(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy, protocol: Protocol?) {
val connectMs = System.currentTimeMillis() - connectStartMs
reportMetric("connect_time", connectMs)
}override fun responseHeadersStart(call: Call) {
responseStartMs = System.currentTimeMillis()
}override fun responseHeadersEnd(call: Call, response: Response) {
// TTFB = 从请求发出到收到第一个响应字节
val ttfb = responseStartMs - requestStartMs
reportMetric("ttfb", ttfb)
}override fun callEnd(call: Call) {
val totalMs = System.currentTimeMillis() - callStartMs
reportMetric("total_time", totalMs)
}
}

注册也很简单:

代码语言:javascript
复制
val client = OkHttpClient.Builder()
.eventListenerFactory { NetworkTimingListener() }
.build()

上线跑了一周数据之后,真实的耗时分布让我很意外:

4G环境 P50耗时分布(某业务接口):

DNS解析 : 120ms (18%)

TCP握手 : 150ms (22%)

TLS握手 : 200ms (30%)

请求发送 : 20ms ( 3%)

服务端处理 : 45ms ( 7%)

响应接收 : 130ms (20%)

────────────────────────────

总计 : 665ms

看到没?DNS + TCP + TLS三项握手就占了70%。服务端才用了7%。如果这个连接是复用的(命中连接池),前三项直接归零,总耗时能降到195ms——性能提升3倍多,一行代码都不用改。

这就是为什么网络优化要从链路开始看,而不是只盯着服务端响应时间。

移动端网络的特殊挑战

桌面端做网络优化相对简单——网络稳定、带宽充裕、延迟可控。移动端就是另一回事了。我总结了四个核心挑战:

挑战一:弱网环境

地铁里、电梯里、地下车库——用户不会因为信号差就不用你的App。实测数据:国内4G网络在地铁场景下,丢包率可以飙到30%以上,RTT从50ms暴涨到2000ms+。

弱网对TCP的影响是灾难性的。TCP的拥塞控制算法(BBR/Cubic)在丢包时会大幅降低发送窗口,一次重传就可能让吞吐量掉90%。更要命的是TLS握手——一旦握手过程中丢包,需要从头来,而TLS 1.2的完整握手需要2个RTT(4次网络交互),在RTT=2秒的弱网下就是4秒起步。

代码语言:javascript
复制
// 弱网检测:通过ConnectivityManager监听网络质量变化
class NetworkQualityMonitor(private val context: Context) {fun isWeakNetwork(): Boolean {
val cm = context.getSystemService(ConnectivityManager::class.java)
val nc = cm.getNetworkCapabilities(cm.activeNetwork) ?: return true// 下行带宽低于150Kbps认为是弱网
val downBandwidth = nc.linkDownstreamBandwidthKbps
if (downBandwidth < 150) return true// 也可以结合实际请求的RTT来判断
return recentAvgRtt > 800 // ms
}fun adaptTimeouts(builder: OkHttpClient.Builder): OkHttpClient.Builder {
return if (isWeakNetwork()) {
builder
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
} else {
builder
.connectTimeout(5, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
}
}
}

挑战二:网络切换

WiFi切4G,4G切WiFi,4G切5G——每次切换,TCP连接就断了。因为TCP连接是通过四元组(源IP:源端口 → 目标IP:目标端口)标识的,IP一变连接就失效了。

这不仅仅是"断了重连"的问题。用户在视频通话中从WiFi切到4G,如果连接断了要重新握手+认证+恢复状态,中间的卡顿是秒级的。QUIC协议用Connection ID替代四元组来解决这个问题,但在传统TCP场景下,最实用的方案是快速检测网络切换并主动清理连接池:

代码语言:javascript
复制
// 监听网络切换,主动清理无效连接
class NetworkSwitchHandler(
private val client: OkHttpClient
) {
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onLost(network: Network) {
// 网络丢失,清空连接池中对应的连接
// 避免后续请求拿到已死的连接还尝试使用
client.connectionPool.evictAll()
}override fun onAvailable(network: Network) {
// 新网络可用,可以预热关键域名的连接
prewarmConnections()
}
}private fun prewarmConnections() {
// 对核心域名提前建立连接(后台线程)
CRITICAL_HOSTS.forEach { host ->
Executors.IO.execute {
try {
client.newCall(
Request.Builder().url("https://$host")
.head().build()
).execute().close()
} catch (_: Exception) { }
}
}
}
}

挑战三:NAT与运营商劫持

运营商的基站会对HTTP流量做NAT(网络地址转换),这本身不是问题。问题在于有些运营商的NAT超时非常激进——空闲30秒就把映射关系回收了。你以为连接还活着(连接池里保着呢),实际上中间设备已经把通道拆了。下次用这个连接发请求,对端收不到,你这边等超时。

更恶心的是HTTP劫持。某些运营商会在HTTP响应中注入广告代码,或者直接篡改DNS响应把你导向缓存服务器。HTTPS能防住内容篡改但防不住DNS劫持——解析出来的IP直接就是错的。

应对方案:HTTPS是底线(防内容篡改);HttpDNS解决DNS劫持(下一篇详聊);连接池的keepAlive时间建议设为20-25秒(低于运营商NAT超时);定期发心跳保活关键连接。

挑战四:异构网络环境

这是移动端独有的痛点。你的用户可能在:2G信号的山区(RTT 500ms+,带宽50Kbps)、东南亚的3G网络(延迟不稳定)、公司内网的代理后面、或者校园WiFi的多层NAT背后。同一套超时配置不可能适配所有场景。

我的建议是搭建网络质量分级体系——根据实时检测到的RTT和带宽动态调整策略:

代码语言:javascript
复制
enum class NetworkGrade {
EXCELLENT,  // WiFi/5G,RTT<50ms
GOOD,       // 4G良好,RTT<200ms
FAIR,       // 4G一般/3G,RTT<500ms
POOR,       // 弱网,RTT<1000ms
TERRIBLE    // 极端弱网,RTT>1000ms
}object NetworkStrategy {
fun getConfig(grade: NetworkGrade): NetworkConfig = when (grade) {
EXCELLENT -> NetworkConfig(
connectTimeout = 3.seconds,
enablePrefetch = true,
imageQuality = Quality.HIGH
)
GOOD -> NetworkConfig(
connectTimeout = 5.seconds,
enablePrefetch = true,
imageQuality = Quality.MEDIUM
)
FAIR -> NetworkConfig(
connectTimeout = 10.seconds,
enablePrefetch = false,
imageQuality = Quality.LOW
)
POOR -> NetworkConfig(
connectTimeout = 15.seconds,
enablePrefetch = false,
imageQuality = Quality.THUMBNAIL,
enableCompression = true
)
TERRIBLE -> NetworkConfig(
connectTimeout = 20.seconds,
enablePrefetch = false,
imageQuality = Quality.NONE,
enableCompression = true,
useCacheOnly = true  // 极端情况先展示缓存
)
}
}

网络性能度量:你至少需要这几个指标

做优化之前得先有数据,否则就是盲人摸象。我建议至少采集这四个核心指标:

1. TTFB(Time To First Byte)

从请求发出到收到第一个响应字节的时间。这是衡量网络链路+服务端处理的综合指标。如果TTFB高但服务端access log显示处理快,说明链路有问题。

2. RTT(Round Trip Time)

一个数据包从客户端到服务端再返回的时间。可以通过TCP握手的SYN-ACK延迟来近似测量。RTT直接决定了握手类操作的耗时下限。

3. 请求成功率

别只看HTTP 200。要区分:网络层失败(DNS超时、连接超时、读超时)vs 业务层失败(HTTP 4xx/5xx)。网络层失败率高说明链路有问题,业务层失败率高是后端的问题。

4. 连接复用率

你的请求有多大比例命中了连接池?复用率高意味着DNS+TCP+TLS的开销被省掉了。实测我们优化前复用率只有40%,优化连接池配置和域名收敛后提升到85%,整体网络耗时直接降了一半。

代码语言:javascript
复制
// 通过EventListener统计连接复用率
class ConnectionReuseTracker : EventListener() {
private var isConnectionReused = falseoverride fun connectionAcquired(call: Call, connection: Connection) {
// 如果没有触发connectStart,说明是复用的连接
isConnectionReused = !hadConnectStart
}override fun connectStart(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy) {
hadConnectStart = true
}override fun callEnd(call: Call) {
MetricsCollector.report(
"connection_reused",
if (isConnectionReused) 1 else 0
)
}
}

OkHttp/Retrofit架构下的优化切入点

有了全链路的认知和度量数据,优化方向就很清楚了。我把OkHttp/Retrofit体系下能做的事情按层次整理一下:

DNS层(第2篇详聊)

→ 自定义Dns接口实现HttpDNS / DNS缓存 / DNS预解析

连接层(第3篇详聊)

→ 连接池调优 / HTTP/2启用 / 域名收敛 / 连接预热 / TLS Session复用

数据层(第4篇详聊)

→ Gzip/Brotli压缩 / Protocol Buffers替代JSON / 增量同步 / 缓存策略

监控层(第5篇详聊)

→ EventListener埋点 / 弱网降级 / 多IP容灾 / 异常告警

这里给几个立刻能用的quick-win:

Quick Win 1:域名收敛

很多项目的API分散在多个子域名上:api.example.com、cdn.example.com、auth.example.com、upload.example.com。每个域名都要单独DNS解析、建立连接。如果能收敛到同一个域名(通过path区分服务),连接复用率能大幅提升。

退一步说,即使不能完全合并,至少确保所有域名共享同一个OkHttpClient实例(共享连接池):

代码语言:javascript
复制
// ❌ 错误做法:每个Retrofit实例用独立Client
val apiRetrofit = Retrofit.Builder()
.client(OkHttpClient())  // 独立连接池
.baseUrl("https://api.example.com")
.build()val cdnRetrofit = Retrofit.Builder()
.client(OkHttpClient())  // 又一个独立连接池
.baseUrl("https://cdn.example.com")
.build()// ✅ 正确做法:共享Client
val sharedClient = OkHttpClient.Builder()
.connectionPool(ConnectionPool(15, 5, TimeUnit.MINUTES))
.build()val apiRetrofit = Retrofit.Builder()
.client(sharedClient)
.baseUrl("https://api.example.com")
.build()val cdnRetrofit = Retrofit.Builder()
.client(sharedClient)  // 共享连接池!
.baseUrl("https://cdn.example.com")
.build()

Quick Win 2:启用HTTP/2

HTTP/2最大的优势是多路复用——一个TCP连接可以同时跑多个请求,不用排队。OkHttp默认支持HTTP/2(对HTTPS连接会自动协商),但需要服务端也支持。确认方法:

代码语言:javascript
复制
// 在EventListener里确认协商结果
override fun connectEnd(
call: Call,
inetSocketAddress: InetSocketAddress,
proxy: Proxy,
protocol: Protocol?
) {
// protocol = h2 表示HTTP/2协商成功
Log.d("Network", "Protocol: $protocol")
if (protocol != Protocol.HTTP_2) {
// 上报,看看哪些域名还不支持H2
reportH2Failure(call.request().url.host)
}
}

Quick Win 3:DNS预解析

App启动时,趁初始化的时间把核心域名提前解析好放到缓存里。这样第一次真正发请求时DNS查询直接命中缓存:

代码语言:javascript
复制
// Application.onCreate 中提前预解析
object DnsPreResolver {
private val criticalHosts = listOf(
"api.yourapp.com",
"cdn.yourapp.com",
"auth.yourapp.com"
)fun prewarm() {
Executors.IO.execute {
criticalHosts.forEach { host ->
try {
// 触发系统DNS查询,结果会被系统缓存
InetAddress.getAllByName(host)
} catch (_: Exception) { }
}
}
}
}

总结与下一篇预告

回顾一下这篇讲了什么:

• 一次HTTP请求的完整链路:DNS → TCP → TLS → HTTP → 响应,握手环节占总耗时70%+

• 移动端四大网络挑战:弱网、网络切换、NAT/劫持、异构网络

• 必须采集的四个指标:TTFB、RTT、成功率、连接复用率

• 三个立刻能用的Quick Win:域名收敛、HTTP/2确认、DNS预解析

最核心的观点:网络优化的第一步不是优化,是度量。没有EventListener的数据,你不知道瓶颈在哪。先把监控加上,跑一周数据,然后再有针对性地优化。

下一篇我们聊DNS优化——这是链路第一跳,也是最容易被忽视的环节。运营商DNS的坑你可能想象不到:域名解析指向错误的CDN节点、DNS缓存被污染、LocalDNS递归查询超时……HttpDNS方案如何解决这些问题,OkHttp自定义Dns接口怎么接入,以及一些大厂实战中的DNS容灾策略,下篇见。

📚 Android网络优化系列 · 第1/5篇

从DNS到连接池,打造极速网络体验

👉 第1篇:Android网络全链路拆解:一次HTTP请求背后的性能陷阱(本篇)

⏳ 第2篇:DNS优化实战:从运营商DNS到HttpDNS的进化之路

⏳ 第3篇:连接优化与复用:让每一次握手都物超所值

⏳ 第4篇:数据压缩与缓存策略:把带宽用到极致

⏳ 第5篇:网络监控与容灾:让网络问题无处遁形

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-05-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 陆业聪 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档