第二章:Retrofit 网络请求流程分析——从创建 Call 到响应的完整链路,DNS 解析在哪

咱们做 Android 开发的,天天都在用 Retrofit 发请求。但你敢拍胸脯说,你清楚一个请求从创建 Call 到最后拿到响应,中间走了多少步吗?我个人习惯,每次遇到网络性能问题,第一件事就是把这整条链路在脑子里过一遍。说白了,你不摸清每个环节的耗时,优化就无从下手。

核心问题:一个 Retrofit 请求,DNS 解析到底发生在哪一步?它和连接耗时是什么关系?

2.1 整体链路:从一行代码到一个 HTTP 响应

先看一个最普通的调用:

// Retrofit 接口定义
interface ApiService {
    @GET("users/list")
    suspend fun getUsers(): List<User>
}

// 使用时
val call = apiService.getUsers()          // 创建 Call
val response = call.awaitResponse()       // 执行请求

你看到的就这两行。但背后,OkHttp 帮你干了大概这么几件事:

  1. Retrofit 将你的 Kotlin 函数调用转为 OkHttpClient.Call 对象
  2. Call 被放入 拦截器链(Interceptor Chain)
  3. 拦截器链依次执行:重试 → 桥接 → 缓存 → 连接(DNS + TCP + TLS) → 发送请求 → 读取响应
  4. 最终 Response 回到你的协程或回调里

嗯,这里面最关键、也是最耗时的,就是第 3 步里的 连接阶段。而 DNS 解析,正是连接阶段的第一步。

Retrofit 网络请求完整流程 & DNS 解析位置 Retrofit 接口 → Call 对象 Call.enqueue() / execute() OkHttp 拦截器链(RealCall.getResponseWithInterceptorChain) RetryAndFollowUp Bridge Cache ConnectInterceptor (DNS + TCP + TLS 在这里) CallServerInterceptor 返回 Response 应用层处理(解析 JSON 等) DNS 解析具体位置 ConnectInterceptor 中调用 `client.dns.lookup(host)` TCP 连接 + TLS 握手紧接其后 整体构成”连接耗时“

2.2 创建 Call —— 这一步几乎不耗时

Retrofit 通过动态代理,把 apiService.getUsers() 转换成 OkHttpClient.Call。这里我提一句,我见过有人觉得这里慢,其实大可不用担心。它只是创建了一个对象,没有网络 IO,连线程切换都没有。真正花钱的地方在后面。

一句话总结:创建 Call 的成本 ≈ 一次 new 对象的时间,完全可以忽略。

2.3 执行 Call —— 拦截器链正式启动

不管你是用 enqueue() 还是 execute(),最终都会走到 RealCall.getResponseWithInterceptorChain()。这方法里裹着一个 ArrayList,按顺序塞了 5 个内置拦截器 + 你自定义的拦截器。然后一个个调过去。

我习惯把这链条想象成流水线。每一个拦截器都有机会“动手脚”:比如重试、添加通用头、读缓存等等。但这里面的老大难,始终是 ConnectInterceptor。

2.4 DNS 解析 —— 真实发生的位置

你跟着代码走一圈就会看到,ConnectInterceptor 里调用了 exchangeFinder.find(),接着走到 RouteSelector 的 next() 方法。在这里,OkHttp 会调用你自己设置的 Dns 实现。默认就是 Dns.SYSTEM,走系统 API——说白了就是 InetAddress.getAllByName()。

注意坑:系统 DNS 一般走 UDP 53 端口,可能会超时(尤其网络不稳定时)。默认超时是 5 秒?不,其实是根据 Socket 超时来的,但一旦 DNS 服务器响应慢,整个请求就卡住了。我曾经排查过一个线上问题:某运营商的 DNS 平均响应 3.2 秒,导致所有接口都慢成狗。

2.5 连接耗时:TCP + TLS 握手

DNS 解析拿到 IP 后,OkHttp 会尝试验证已有连接池里有没有可复用的连接。没有?那就新建 TCP 连接。如果用了 HTTPS,还得再加一次 TLS 握手。这两步加起来,往少了说几十毫秒,多的能有几秒。

常规耗时参考(WiFi 环境)
阶段典型耗时备注
DNS 解析10~200 ms取决于 DNS 服务器和网络
TCP 连接20~100 ms一个 RTT
TLS 握手100~500 ms根据加密套件,通常 1~2 个 RTT
请求传输 + 响应依数据量而定这个优化不在本章讨论

2.6 一个小实验:验证 DNS 耗时

你可以在 Retrofit 初始化时,给 OkHttpClient 加一个自定义 Dns:

val client = OkHttpClient.Builder()
    .dns(object : Dns {
        override fun lookup(hostname: String): List<InetAddress> {
            val start = System.nanoTime()
            val result = Dns.SYSTEM.lookup(hostname)
            val elapsed = (System.nanoTime() - start) / 1_000_000
            Log.d("DNS", "$hostname 解析耗时 $elapsed ms")
            return result
        }
    })
    .build()

跑一下你的 App,看看那些接口的 DNS 用了多长时间。你会吓一跳的——有些 CDN 域名解析能到 500+ ms。这就是优化的起点。

2.7 本章小结

Retrofit 的请求链路,本质就是 OkHttp 的拦截器链。DNS 解析藏在 ConnectInterceptor 里,是最容易被忽略的耗时元凶。下次你再觉得某个接口慢,别急着优化 JSON 解析或者压缩图片——先看看 连接阶段 花了多久。连接慢,后面再快也无济于事。