3. OkHttp 默认 DNS 解析实现:Dns 接口、系统调用与性能瓶颈
聊到 DNS 解析,很多人第一反应就是「哦,不就是域名转 IP 嘛」。但在 Retrofit + OkHttp 的体系里,这一层的效率直接决定了你首包到达的时间。我见过不少项目,优化了半天业务代码,结果发现每次请求都要等 DNS 几百毫秒 —— 这就是典型的「根上没弄对」。
这一节我们就把 OkHttp 默认怎么调 DNS、底层做了什么、以及它到底慢在哪,彻底说清楚。
Dns 接口:OkHttp 留给你的扩展点
OkHttp 并没有把 DNS 写死在核心代码里。它抽象了一个非常干净的接口:
public interface Dns {
List<InetAddress> lookup(String hostname) throws UnknownHostException;
Dns SYSTEM = hostname -> {
// 底层调 InetAddress.getAllByName
return Arrays.asList(InetAddress.getAllByName(hostname));
};
}
你瞧,就这么一个方法。传入域名,返回 InetAddress 列表。默认实现 Dns.SYSTEM 直接委托给了 InetAddress.getAllByName。这个静态方法最终走到 getaddrinfo —— 标准的 POSIX 系统调用。
Dns 接口就能绕开真实网络。我在项目里经常用这个方法写 DNS 缓存的测试用例。
系统 DNS 调用方式:阻塞、串行、不可控
为什么要单独拿出来说?因为 InetAddress.getAllByName 是 同步阻塞 的。没错,它发起一个 Socket 查询(通常是 UDP 到 53 端口),然后当前线程就挂在那等结果。网络好的时候几十毫秒,网络差或者 DNS 服务器响应慢的时候,几秒都有可能。
OkHttp 默认情况下,每个新域名都会走这个流程。而且,它不会跨请求复用缓存 —— 除非你显式配置了 DNS 缓存。你想想看,如果一个页面有 10 个不同的域名(CDN、图片、API 网关等),第一次打开时每个域名都要等一次 DNS,那启动时间直接就起飞了。
核心问题总结:
- 调用方式是同步阻塞的
- 没有内置的本地缓存(依赖系统 DNS 缓存,但不可控)
- 没有超时设置(默认依赖系统超时,通常很长)
- 不支持并发解析优化
存在的性能瓶颈:从 ARP 到 ANR
我们来拆解一下,这些「缺陷」在真实场景里到底引发了什么。直接看一个我遇到的典型案例:
有一段时间用户的 ANR 率突然升高,堆栈指向 OkHttp 的连接建立流程。查了半天,发现部分用户的网络环境下,DNS 解析耗时竟然超过 10 秒。为什么?因为他们公司的网络出口走了很慢的公共 DNS,而 OkHttp 默认没有任何兜底措施,直接卡死了主线程(如果使用的是同步请求)。
下面这张图能帮你理清整个调用链路和瓶颈点:
看到红色节点那一层了吗?那就是性能黑洞。别小看这几百毫秒,在移动端网络波动时,它能拖垮整个用户体验。
具体瓶颈拆解
| 瓶颈维度 | 具体表现 | 对 Retrfit 的影响 |
|---|---|---|
| 阻塞调用 | lookup() 内部同步等待 UDP 响应,线程挂起 | 主线程请求直接导致 ANR |
| 无内置缓存 | 每次 lookup 都触发系统调用,不缓存结果 | 相同域名反复解析,浪费带宽和时间 |
| 超时不可控 | 依赖系统默认超时(通常 5-30 秒) | 网络差时请求排队,队列积压超时 |
| 并发无优化 | 对同一个域名,多个线程会同时发起多次 lookup | 无谓并发增加 QPS,可能触发限流 |
我曾经踩过的一个坑:线上有个页面同时请求三个子域名资源,第一次打开时每个子域名都触发一次 DNS 解析(由于系统 DNS 缓存还没建立),三次耗时加起来超过 3 秒。用户连续点击后退再进入,有些场景下系统 DNS 缓存会被清除,又来一遍。后来我们换成自建 DNS 缓存 + 预热,首屏时间直接降了 40%。
为什么 Retrofit 用户更应该关注?
Retrofit 是基于 OkHttp 的,所以所有请求的 DNS 解析都走上面这一套。而且 Retrofit 的 Converter 和 CallAdapter 往往让开发者忽略网络层细节。你以为只是序列化慢?其实很多时候是 DNS 在拖后腿。
我个人习惯在项目刚起步时就打印一下 DNS 耗时日志,很简单:
OkHttpClient client = new OkHttpClient.Builder()
.dns(hostname -> {
long start = System.nanoTime();
List<InetAddress> addresses = Dns.SYSTEM.lookup(hostname);
long cost = (System.nanoTime() - start) / 1_000_000;
Log.d("DNS_DEBUG", hostname + " -> " + addresses + " cost=" + cost + "ms");
return addresses;
})
.build();
把这段代码贴到你的 Retrofit 配置里跑两天,你就能发现有多少「隐藏的慢」。
一句话总结:OkHttp 的默认 DNS 实现是一个严格的同步阻塞模型,没有任何智能缓存和超时兜底。不优化它,你后面做的连接池、重试、拦截器优化都可能被它拖入泥潭。
你要记住,DNS 是网络请求的第一个关卡。我在接下来的章节会给你展示怎么用自建缓存、异步解析、自定义 Dns 实现等方式彻底解决它。但首先,你得承认 —— 默认的 Dns.SYSTEM 真的不够聪明。嗯,至少对性能敏感的 App 来说,它只能算及格。