
随便在任何一个Android项目的gradle文件里搜一下基本都能看到implementation com.squareup.okhttp3:okhttp:x.x.x这一行。说OkHttp是Android生态里最主流的HTTP客户端一点都不夸张。但有意思的是很多人对它停留在会用的层面——会构建Request、会调enqueue、会处理Callback真到了线上出现超时、连接复用异常、DNS解析慢这类问题就说不清楚底层到底发生了什么。这篇文章不打算从零讲怎么发一个GET请求那是官方文档已经写明白的事。我想聊的是OkHttp真正值钱的部分它为什么快、连接池和Dispatcher如何协同、拦截器链的执行次序到底对业务有什么影响、以及我在生产环境里踩过的那些和超时、缓存、DNS相关的坑。无论你是刚接触OkHttp的新手还是已经用了好几年的老手这篇文章应该都能给你一些新的视角。1. 从一次线上事故说起为什么OkHttp请求会卡死之前我们有个核心接口在版本发布后突然出现大面积超时用户反馈页面一直转圈。第一反应是服务端出了问题但查看监控发现服务端平均响应时间只有80毫秒。也就是说请求发出去了服务端也处理了但客户端就是没拿到结果。后来抓日志分析发现一个规律出问题的请求都是首次请求。再往深挖是OkHttp在建立新连接时卡住了TCP握手都没完成。为什么因为我们改了DNS配置把原本走系统解析的域名切到了自建的HTTPDNS而恰好那个版本的HTTPDNS服务有个bug在特定网络环境下会一直等待响应导致connectTimeout形同虚设。这个案例是我觉得讲OkHttp最好的切入点——它的连接管理、超时控制、DNS解析这些机制平时都隐藏在正常的请求里一旦出错就是疑难杂症。而理解这些机制恰恰是排查这类问题的前提。OkHttp的核心设计目标其实就一句话让HTTP请求尽可能快、尽可能稳、尽可能省资源。围绕这个目标它做了三件大事连接池复用TCP连接避免频繁三次握手Dispatcher调度器管理并发请求控制线程资源消耗拦截器链把请求处理过程拆成一条流水线方便扩展和定制。这三件事也是本文要重点拆解的内容。2. 连接管理OkHttp最快的秘密藏在连接池里2.1 TCP连接复用的核心价值HTTP协议有个很烦人的特点早期版本里每次请求都要新建一个TCP连接。而TCP连接的建立需要三次握手HTTPS还得再加一次TLS握手。在弱网环境下一次握手可能就要几百毫秒甚至更久。OkHttp解决这个问题的方案是连接池。它会把已经建立但暂时不用的连接缓存起来下次请求同一个域名时如果池里有可用连接就直接复用省去握手开销。连接池的默认配置是最大空闲连接数5个每个连接空闲超过5分钟就会被清理。这个机制听起来简单但实际效果非常显著。尤其对于那种频繁请求接口的应用连接复用能把请求耗时降低一个数量级。我们在一个IM项目里做过统计启用连接池之后消息拉取接口的平均耗时从300毫秒降到了80毫秒几乎都是省在握手阶段。2.2 连接池的拆解存活时间、清理线程与连接淘汰连接池内部其实就是一个DequeRealConnection配合一个专门的清理线程ConnectionPool.cleanupRunnable来维护。每次请求结束连接不会立即关闭而是归还到池子里。清理线程会周期性扫描如果连接空闲时间超过keepAliveDuration默认5分钟直接关闭如果空闲连接数超过maxIdleConnections默认5个淘汰最久未使用的如果池子里没有连接需要清理线程会进入等待状态直到有新的归还动作。一个比较容易被忽略的点是连接池的复用是有条件的。OkHttp复用一个已有连接时会检查Host、端口、协议HTTP/1.1还是HTTP/2、代理设置是否一致。任何一个不匹配都不能复用。所以如果你同一个客户端同时访问了域名和IP直连这两个请求是各走各的连接池的。2.3 关于连接池的三个容易踩的配置误区连接池虽然好用但配置上我有几个亲身踩过的坑。第一ConnectionPool的构造参数是最大空闲连接数和keepAlive时间这个5分钟不是硬性保证。如果服务端的keepAlive超时时间比你短服务端会先关闭连接。此时OkHttp会在第二次请求时发现连接已死自动重试建连。但如果你配置了retryOnConnectionFailure(false)重连就不会发生请求直接失败。第二连接池的复用是按空闲连接来判断的不是按最近使用时间。如果一个连接一直在被使用它的keepAlive时间会不断刷新。但如果你的业务是那种周期性轮询间隔超过5分钟那么每次轮询都会新建连接。这种情况下把keepAlive时间调长是有效果的。第三HTTP/2开启后连接池的复用粒度会变得很粗。HTTP/2支持多路复用一条连接上可以并发跑多个请求所以理论上一个Host只保留一条连接就够了。但OkHttp默认依然保留了多个连接的余量这是为了兼容HTTP/1.1和HTTP/2混跑的中间状态。3. 原理篇Dispatcher、拦截器与请求执行流程3.1 Dispatcher控制并发的隐形调度器Dispatcher是OkHttp里负责调度请求的组件但它其实非常简单——三个队列一个线程池。runningSyncCalls存同步请求runningAsyncCalls存执行中的异步请求readyAsyncCalls存等待执行的异步请求。异步请求通过enqueue()提交时Dispatcher先检查当前运行中的请求数是否超过maxRequests默认64以及目标同一个Host的连接数是否超过maxRequestsPerHost默认5。如果都满足就会把请求丢给线程池执行否则放入等待队列等有请求完成后再唤醒。Dispatcher自带的线程池是用SynchronousQueue构造的这意味着如果没有空闲线程新任务会直接创建新线程。所以Dispatcher其实没有最大线程数上限理论上可以达到maxRequests64的并发量。3.2 同步请求与异步请求的抉择很多初学者在这里有个误解以为execute()会创建一个新线程。其实execute()是当前线程阻塞执行如果你在主线程调用OkHttpClient.execute()那大概率会收到NetworkOnMainThreadException除非你在子线程里调用。enqueue()则是把请求交给Dispatcher的线程池去执行回调在OkHttp的线程池中执行所以回调里不能直接更新UI必须切到主线程。这个细节看似小但经常有人踩在Callback里直接操作View然后偶发性崩溃。我日常开发里的建议除非有特殊需求比如同时发起多个请求并需要阻塞等待全部返回否则一律使用enqueue()做异步请求。同步请求容易写出一堆线程管理代码而且阻塞当前线程会增加死锁风险。3.3 拦截器链从业务到Socket的完整执行路径拦截器是OkHttp最灵活的设计。每一个请求都会经过一条拦截器链链条上每个节点都有机会在请求发送前、响应返回后做一些操作。官方内置了五大拦截器按执行顺序分别是ApplicationInterceptor应用拦截器通过addInterceptor()添加RetryAndFollowUpInterceptor重试和重定向拦截器BridgeInterceptor桥接拦截器负责处理请求头、Cookie、gzip等CacheInterceptor缓存拦截器ConnectInterceptor连接拦截器负责从连接池获取连接CallServerInterceptor网络拦截器通过addNetworkInterceptor()添加的拦截器在这附近执行最终负责写请求和读响应我把这个链条拆细一点方便理解每个环节的作用ApplicationInterceptor是你业务代码里塞进去的可以在请求真正发出前做统一处理——加公共参数、加签名、做日志输出、打点统计。它看到的是完整的Request和最终Response但注意它拿到的Response可能是经过重定向之后的最终响应而非第一次请求的302。RetryAndFollowUpInterceptor负责处理重定向和连接失败重试。比如一个请求返回302这个拦截器会构造一个新的Location请求并再次发起。如果请求过程中连接断了且请求体可以重复它还会自动重试一次。BridgeInterceptor是隐身功臣。它负责把用户没有显式设置的Header补齐比如缺失时自动添加Content-Length、Host、Connection: keep-alive、Accept-Encoding: gzip等。还会对响应做gzip解压。CacheInterceptor的实现依赖于你在构建OkHttpClient时配置的Cache对象。它按照HTTP缓存规范来判断是否可以走到本地缓存还是需要发请求到服务端验证新鲜度。ConnectInterceptor是个很轻的环节核心动作就是从连接池中获取一个可用连接。CallServerInterceptor是最后一个拦截器它真实地执行Socket的读写。HTTP/1.1走Http1ExchangeCodecHTTP/2走Http2ExchangeCodec把请求写到流里然后读取响应返回。每一个请求的执行就是这六个环节的一次链条传递。理解这个顺序最大的价值在于调试时你能准确判断我的自定义拦截器到底在哪个位置执行的影响是什么。4. 超时配置与连接重试你忽略的细节正在拖垮性能4.1 三个超时参数各自管什么OkHttp里有三个超时配置connectTimeout、readTimeout、writeTimeout。很多人知道有这三个参数但未必说得清楚它们的边界。connectTimeout建立TCP连接的超时时间。如果在这个时间内TCP握手没有完成直接抛ConnectException。这里要注意DNS解析的时间是不算在connectTimeout里的。readTimeout从Socket读取数据的超时时间更准确地说是两次读取之间的最大空闲时间间隔。不是整个响应体读不完就会超时而是只要持续有数据到达就没有问题。writeTimeout向Socket写数据的超时时间同样是两次写入之间的最大休息时间不是整个请求体写完的总时限。这三个参数默认都是10秒。但坦白说10秒对于大多数移动端业务都偏长了。线上移动网络环境复杂一个请求如果10秒没响应用户早就退出页面了。我们通常把connectTimeout设为5秒、readTimeout设为10秒、writeTimeout设为10秒再根据具体接口的耗时分布做微调。4.2 超时后的重试机制以及重试的安全边界OkHttp的RetryAndFollowUpInterceptor不仅处理重定向也处理连接失败后的自动重试。这个重试有个大前提请求体必须是可重复读取的。如果是流式请求体一旦写到一半连接断了重试时就无法重新读取请求体就直接报错。很多人会在这里栽跟头上传大文件或二进制流时连接在中间断开OkHttp无法重试日志里会出现UnrecoverableIOException: Attempted to retry upload after an exception。这个错误本身不可怕但它提示你重试机制是有边界的别把所有希望寄托在自动重试上。另一个值得注意的点是OkHttp的重定向默认最多跟随20次。如果服务端配置了一个A跳到B、B跳到A的循环重定向OkHttp会抛ProtocolException: Too many follow-up requests。这种情况通常是服务端的配置问题但客户端也要有日志监控否则很难定位。4.3 实测中怎么判断超时问题的根因超时问题最麻烦的是判断故障点。我提供一个最常用的排查思路先用curl -v验证服务端是否正常再确认是连接建立超时还是读取超时——这个问题可以通过OkHttp的异常类型区分ConnectException、SocketTimeoutException、UnknownHostException分别对应不同阶段。最有效的方式是在应用拦截器里给每次请求打点记录分段时间DNS解析前、连接建立前、连接建立后、请求发送完成、响应读取完成。OkHttp的EventListener接口就是干这个的它可以监听dnsStart、connectStart、requestBodyEnd、responseBodyEnd、connectionAcquired等上百个回调点。这个接口是排查性能问题的利器建议每个项目都接入。我在实际项目里接过一次EventListener第一次跑就发现了一个隐藏了半年的问题某个接口的writeTimeout频繁触发原因是请求体太大Socket缓冲区写不完。加了监控之后接口瘦身了两次彻底根治。5. 缓存与资源复用让OkHttp替你省流量5.1 OkHttp缓存的生效条件远不止配个CacheOkHttp的磁盘缓存需要你手动构造val cache Cache(File(cacheDir), 50 * 1024 * 1024) // 50MB val client OkHttpClient.Builder() .cache(cache) .build()但配了Cache不代表所有请求都走缓存。OkHttp的缓存策略严格遵循HTTP协议规范。一个响应能不能缓存由响应头决定Cache-Control: max-age600允许缓存缓存的保鲜期是600秒。Cache-Control: no-cache允许缓存但每次使用前需要向服务端重新验证。Cache-Control: no-store禁止缓存。ExpiresHTTP/1.0时代的过期时间头优先级低于Cache-Control。响应带有Set-Cookie头时OkHttp默认不会缓存对应响应。最重要的一个限制只有GET请求的响应才会被OkHttp缓存POST请求一律不缓存。这个限制很多人不知道他们配置了缓存后发了个POST请求发现响应没缓存就以为是OkHttp的问题。实际上这是HTTP协议本身的语义——POST一般被认为是有副作用的操作不应该被透明缓存。5.2 条件请求与304缓存命中的最后一环OkHttp缓存命中后响应会直接从磁盘读取不会发起网络请求这个速度是毫秒级的。但如果缓存的响应已经过期OkHttp也不会直接弃用而是发起一个条件请求带上If-None-Match对应ETag或If-Modified-Since对应Last-Modified头。服务端如果资源没变返回304 Not ModifiedOkHttp会续期本地缓存继续复用。这个机制叫再验证它消耗一点网络请求成本换来的是大量省流。这里有个优化点如果你的接口请求头里带着认证信息Authorization而服务端的缓存策略又没处理好Vary头缓存可能会串号。这是一类比较隐蔽的问题排查手段是关闭缓存后对比响应。5.3 缓存与Cookie、认证的配合问题如果你的接口依赖登录态响应里带了Set-CookieOkHttp的CookieJar会处理Cookie的保存。但缓存策略上要注意带Authorization头的请求响应可以被缓存但再验证时必须带上同样的Header。如果服务端对带认证的请求返回的Cache-Control是public那缓存的内容可能被下一个没有认证的请求复用造成信息泄露。我们的做法是凡是涉及用户私有数据的接口明确要求服务端返回Cache-Control: no-store从协议层面杜绝缓存污染。而公开接口比如首页推荐位则尽量设置max-age让客户端能直接命中缓存降低服务端压力。6. 实际生产中的坑与调配思路6.1 连接池复用率太低怎么调优如果你发现接口耗时有明显波动、且大量请求都处在TCP建连阶段多半是连接池复用率低。排查方式是通过EventListener统计connectionAcquired和connectionReleased之间的时间差再结合connectionReused是否为true来判断。调优方向有几个检查keepAlive设置如果默认5分钟和你业务的轮询节奏不匹配可以加大到10分钟或更长检查是否每次请求都new了一个OkHttpClient实例。这是很常见的错误会导致连接池完全失效。OkHttpClient应该设计成单例全局复用如果业务场景是短小请求密集可以考虑开启HTTP/2一条连接就能扛住并发天然提升复用率。6.2 HTTP/2适配多路复用与性能提升OkHttp从3.x开始就内置了HTTP/2支持。HTTPS链接会在TLS握手阶段通过ALPN协商协议版本如果服务端支持HTTP/2OkHttp会自动切过去。HTTP/2带来的核心收益是多路复用多个请求共享一条TCP连接并发请求之间互不阻塞彻底解决了HTTP/1.1的队头阻塞问题。但HTTP/2也不是没有问题。它对网络稳定性的要求更高中间代理如果不懂HTTP/2会出现连接被重置的异常。此外HTTP/2的多路复用可能导致一个慢请求占住连接资源影响同一连接上的其他请求。OkHttp内部的连接池会动态调整连接数来缓解这个问题但在极端场景下比如弱网下行、上行不对称网络HTTP/2的性能劣势也会暴露。我的建议是如果你的服务端开启HTTP/2就先用着但保留HTTP/1.1的降级路径。OkHttp的协议协商是自动完成的服务端不支持就自动降级你不需要额外配置。6.3 日志与拦截器的生产级用法日志是排查问题最重要的手段。OkHttp提供了HttpLoggingInterceptor但这个拦截器有个坑它如果把响应体都打出来对于大JSON响应日志会非常长反而影响性能。我的生产级用法是在Debug环境用HttpLoggingInterceptorLevel设为BODY方便调试在线上环境关闭HttpLoggingInterceptor改用自定义的轻量级打点拦截器只记录URL、状态码、耗时上报到监控平台使用EventListener而不是日志输出来定位性能问题。另外要特别提醒不要把敏感信息放到日志里。线上环境一旦开了BODY级别日志用户手机里的日志文件可能包含完整的Token、手机号等信息这是很严重的合规风险。6.4 DNS解析的坑以及自定义DNS的方案OkHttp默认使用系统DNS也就是InetAddress.getAllByName()。这带来几个问题系统DNS在部分定制ROM下可能被劫持或污染运营商DNS解析速度不稳定劣化网络体验传统DNS不支持负载均衡和灰度发布。OkHttp提供了Dns接口你可以自定义解析逻辑。比如接入HTTPDNS或自建的DNS-over-HTTPS服务。自定义DNS的典型实现val client OkHttpClient.Builder() .dns(object : Dns { override fun lookup(hostname: String): ListInetAddress { // 先从本地缓存/HTTPDNS服务获取IP // 抛出UnknownHostException则请求失败回调走onFailure } }) .build()要注意自定义DNS解析的结果会被OkHttp缓存到连接池对应的连接上。如果你返回了一个已经失效的IP请求会失败。所以自定义DNS一定要有异常回退逻辑解析失败时回落到系统DNS避免出现不可用的情况。7. 插件的生态与选型为什么最终都回到OkHttp7.1 Retrofit没它还叫Android开发吗Retrofit是建立在OkHttp之上的REST API框架核心价值是把接口定义变成注解自动解析响应体。它本身不做网络请求所有网络IO全交给OkHttp。Retrofit的Call.Factory可以让你把自定义的OkHttpClient注入进去这样拦截器、超时配置、缓存都能无缝共享。我在新项目里的标配是Retrofit OkHttp Kotlin协程。Retrofit从2.6.0开始支持了suspend fun配合协程代码可以写得非常优雅interface ApiService { GET(user/info) suspend fun getUserInfo(Query(id) id: String): UserInfo }Retrofit是模板方法模式的极致体现它把变化的部分抽象成注解和Converter把不变的逻辑固化在内部。而OkHttp则是它的最终执行者。两者配合开发效率非常高。7.2 OkHttp的协议行为与Interceptor扩展生态OkHttp的生态里有一堆开箱即用的拦截器。除了官方提供的日志拦截器社区还有很多好用的库okhttp-profiler配合Chrome DevTools查看网络请求chucker一个应用内的网络调试面板okhttp-signing自动给请求加签名okhttp-dns自定义DNS生态的基础库。拦截器就像洋葱每层只干一件事。这种设计让OkHttp的可扩展性极强几乎所有网络层面的通用逻辑都能通过拦截器实现而不需要改动核心代码。我在项目里写过一个全局的RequestIdInterceptor在应用拦截器里给每个请求加上X-Request-ID头然后在网络拦截器里把同一个ID打印到日志。这样把客户端请求日志和服务端访问日志串起来排查问题的效率翻倍。7.3 数据链路全链路追踪的闭环建议如果你的服务端也用了分布式链路追踪比如SkyWalking、Zipkin可以让OkHttp在发起请求时生成一个traceId和spanId放进Header传给服务端。服务端收到后把日志与客户端日志关联起来。这是排查用户反馈某个接口慢但服务端明明很快这类问题的最有效手段。不过这部分有点超出单纯OkHttp的范畴属于全链路可观测性的内容。你可以先从EventListener和日志拦截器入手把客户端这半条链路摸清楚再做服务端对接。写在最后OkHttp不是那种看一遍文档就会精通的库。它设计得很精巧很多行为都藏在默认配置和拦截器链背后。对它的理解每深一层线上问题的排查效率就高一层。尤其是拦截器链、连接池和超时这三个部分几乎覆盖了日常开发里八成的网络疑难杂症。我这几年做移动端网络优化最深的体会是不要先怀疑OkHttp先去看自己的配置和用法对不对。大多数OkHttp问题其实都是没理解它的协议行为和边界条件。把它当做一个可以拆解的组件去对待你会收获比会用多得多的东西。