聊到Android网络层,绕不开OkHttp。这个由Square团队开源的HTTP客户端,几乎成了整个Android开发圈的事实标准——Retrofit的网络请求默认交给它执行,Glide的图片下载底层也借了它的力,市面上绝大多数App的请求背后都有它的影子。这篇文章不打算写入门教程式的使用方法,而是从框架设计者的视角,把OkHttp的请求执行流程、拦截器机制、连接池复用、缓存策略这些核心模块逐个拆开来讲,顺带把我实际项目中踩过的坑也一并交代清楚。
适合谁看?如果你正在被OkHttp源码面试题折磨,或者已经在项目里用了OkHttp,但遇到超时、连接失败、内存上涨这类问题只能靠重启应用暂时躲过去,那这篇文章应该能帮你把思路彻底捋顺。我会尽量用说人话的方式,把每个机制背后的“为什么”讲清楚,而不是单纯贴几段源码了事。
1. OkHttp到底解决了什么问题
1.1 没有OkHttp之前,Android网络层有多难受
Android早期官方推荐的网络库是HttpURLConnection,后来又有Apache HttpClient,这俩用起来都算不上顺手。HttpURLConnection的功能极其基础,没有连接池的概念,每次请求都要重新走一遍TCP三次握手,遇到弱网环境简直是灾难。而且它API设计得也很别扭,想要自定义超时、加请求头、处理重定向,都得写一堆样板代码。
Apache HttpClient功能倒是丰富,但它实在太重了,而且和Android系统版本的兼容性问题一直存在,Google后来干脆在Android 6.0里把Apache HttpClient移除了。如果你经历过那个年代,一定记得为了在Android项目里用HttpClient而手动添加依赖、还要处理各种类冲突的糟心事。
OkHttp就是在这个背景下出现的。它就像是给Android开发者定制的一把瑞士军刀,把HTTP协议层面那些琐碎但至关重要的能力都做了完整的封装:连接池自动复用TCP连接、透明的GZIP压缩、强大的响应缓存、基于拦截器的灵活扩展。最关键的是,它把这些能力藏在了简单直观的API背后,日常使用只需要几行代码就能发一个请求,完全不用关心底层细节。
1.2 OkHttp的核心能力清单
我在跟团队新人聊天时经常说,理解一个框架最好的方式不是先看它怎么用,而是先看它解决了哪些问题。OkHttp的核心能力可以归纳成这样六点:
第一,HTTP/2与连接复用。它自己维护了一个连接池,多个请求可以共享同一个底层TCP连接,在HTTP/2下还能多路复用,大幅降低网络延迟。第二,拦截器链设计。这是OkHttp的灵魂所在,网络请求从准备到发送、从响应回收到重试处理,全部通过拦截器串联,开发者也能非常优雅地插入自定义逻辑。第三,透明的GZIP压缩。请求体自动压缩、响应体自动解压,对调用方完全透明。第四,响应缓存。通过标准的HTTP缓存协议,它可以帮你把一次请求的响应缓存下来,下次同样的请求直接从缓存读取,省流量又提速度。第五,连接失败自动重试与重定向处理。如果连接失败会尝试备用路由,遇到301、302也会自动跟进重定向。第六,WebSocket支持。用同一套调用模型支持长连接场景。
把这些能力串起来看,你会发现OkHttp做的正是“把HTTP协议真正用好”这回事。面试时候被问“为什么Retrofit要选OkHttp做底层”,标准答案其实就是这句话:因为HTTP协议里那些提升性能的机制,OkHttp已经帮你处理得明明白白了。
2. 从一次请求开始,看OkHttp的执行链路
2.1 发一个最简单请求,背后有哪些对象在协作
不论用OkHttp发什么请求,最基本的三个角色是Request、Call和Response。你可以这样理解它们的关系:Request描述“我想发什么”,Call描述“这次请求的执行过程”,Response描述“服务器回给我什么”。下面的代码是最典型的同步请求写法:
val client = OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(15, TimeUnit.SECONDS) .build() val request = Request.Builder() .url("https://api.github.com/repos/square/okhttp") .header("Accept", "application/json") .build() // 同步请求,会阻塞当前线程 client.newCall(request).execute().use { response -> if (!response.isSuccessful) throw IOException("Unexpected code $response") println(response.body?.string()) }有些朋友第一次看这段代码时会有个疑问:newCall()到底做了什么?实际上newCall()返回的是一个RealCall对象,它才是真正干活的家伙。RealCall内部持有OkHttpClient、Request以及一个Dispatcher的引用,后续所有执行逻辑都是从这里展开的。
同步请求用起来简单,但有个硬约束:不能在主线程使用。Android的主线程一旦被网络耗时操作阻塞,轻则界面卡顿,重则直接抛NetworkOnMainThreadException崩给你看。所以实际项目里更多用异步方式。
2.2 Dispatcher:OkHttp的线程调度中心
异步请求调用的是enqueue()方法,传入一个Callback对象,请求完成之后回调会在后台线程触发,不会堵住UI线程。
client.newCall(request).enqueue(object : Callback { override fun onFailure(call: Call, e: IOException) { // 请求失败,注意这里还是在子线程 } override fun onResponse(call: Call, response: Response) { // 请求成功,但仍然是子线程,不能直接操作UI } })你可能会好奇,异步请求的“异步”是谁实现的?答案是Dispatcher。它是OkHttp自己管理的线程池调度器,核心作用有三个:维护同步请求队列、维护异步请求队列、控制最大并发数。
Dispatcher默认有两组关键参数,maxRequests是同一时刻异步请求的最大并发数,默认64;maxRequestsPerHost是同一Host的最大并发数,默认5。这两个参数很有意思,它们决定了你的App在大量请求同时发起的场景下,系统资源会不会被打满。我见过一些线上问题,就是因为有人把maxRequests调得特别大,结果瞬间几百个线程同时抢CPU,应用卡成PPT,这种场景往往不是网络问题,而是并发调度失控。
Dispatcher内部使用的是ExecutorService线程池,但默认的线程池没有核心线程,最大线程数是Integer.MAX_VALUE,配合SynchronousQueue实现了一种“来一个任务就起一个线程,任务结束就回收线程”的策略。如果你对线程池的参数比较熟,会意识到这是一种适合大量短时并发任务的配置。想要自定义线程池,可以通过OkHttpClient.Builder.dispatcher()传入自己构建的Dispatcher。
2.3 请求从发起到响应,完整链路是怎样的
一次请求真正发出去之前,需要经过好几道关卡。我画一张文字版的执行链路图:
应用层调用 execute/enqueue ↓ 应用拦截器(addInterceptor 添加的) ↓ RetryAndFollowUpInterceptor(重试与重定向) ↓ BridgeInterceptor(桥接请求头/响应体) ↓ CacheInterceptor(缓存读写) ↓ ConnectInterceptor(建立连接) ↓ CallServerInterceptor(真正与服务器交换数据) ↓ 返回 Response 给调用方这个链路中的每一环都是一个拦截器,它们在Response真正返回给调用方之前形成了一个“洋葱模型”或者说“流水线”。请求从外往内一层层穿透,响应从内往外一层层返回。这就是OkHttp最经典的设计——拦截器链。理解了这条链,你对OkHttp的执行机制就有了整体把握,接下来逐个拆解这些拦截器,思路会更清晰。
3. 拦截器链:理解OkHttp的设计灵魂
3.1 责任链模式:把复杂的请求处理拆成一个个独立关卡
拦截器链的本质是责任链模式。把“发一次请求”这件麻烦事拆成多个环节,每个环节只关注自己的那一小块职责,然后像流水线一样依次执行。这样做的好处特别明显:新增一个能力不需要改原有的代码,只需要往链里插入一个新的拦截器,扩展性和可维护性都很好。
你可以把拦截器链想象成公司里的审批流程。你提交一个报销单,它要依次经过组长、经理、财务的审批。每个人只检查自己负责的那一项,审批完之后传给下一个。如果你想加一道“法务审批”,不用把之前的流程推倒重来,在链上多挂一个节点就行。OkHttp的拦截器链也是同一个道理。
在源码层面,每个Intercept接口的核心方法就一个:
interface Interceptor { fun intercept(chain: Interceptor.Chain): Response }chain对象代表连接当前拦截器和下一个拦截器的纽带。当前拦截器处理完自己的逻辑之后,调用chain.proceed(request)把请求传给下一个拦截器,拿到Response再处理后返回。这样的设计天然支持“请求前做点事、拿到响应后再做点事”的AOP式编程。
3.2 内置的五个拦截器分别干了什么
先说说RetryAndFollowUpInterceptor。它负责两件事:连接失败后的重试,以及HTTP重定向的自动跟进。重试不是无脑重试,如果请求虽然失败但已经向服务器写入了数据,再重试就可能造成重复提交,所以这种场景OkHttp是默认不重试的。重定向则是根据响应码和Location头再次发起新请求,默认最多跟进20次,避免死循环。
然后是BridgeInterceptor。它负责把开发者写的Request“翻译”成真正满足HTTP协议规范的请求。举个例子,你没有手动设置Content-Length和Host头,它会帮你补充;你请求的是网页内容,它会默认加上Accept-Encoding: gzip,响应回来时再自动帮你解压。这个拦截器默默地做了很多“填坑”工作,所以你在代码里完全不需要手动处理gzip解压。
CacheInterceptor是缓存模块的入口。它会根据请求的缓存策略检查本地有没有可复用的缓存,有就直接返回,没有才继续往下走。拿到响应之后,它还会判断这个响应能否被缓存、缓存多久。这块我后面专门用一章来细说。
ConnectInterceptor负责建立TCP连接,或者从连接池里捞一条可复用的连接。这里要重要提一点,HTTP/1.1下一条连接同一时刻只能处理一个请求,所以如果一个Host的并发请求超过连接数,队列里就得排队。StreamAllocation这个类就是负责连接分配和复用的关键角色。
最底层是CallServerInterceptor。它通过连接将请求头发送到服务器,读取响应状态行、响应头、响应体,然后封装成Response对象往上返回。这一层已经触及了HTTP协议最底层的报文交互。
3.3 自定义拦截器的最佳实践
OkHttp允许我们通过两种方式插入自定的拦截器:addInterceptor()添加的是应用拦截器,addNetworkInterceptor()添加的是网络拦截器。它们的区别很关键,应用拦截器位于链的最外层,无论缓存命中与否都会执行;网络拦截器位于缓存之后、连接之前,只有真正发起网络请求时才会执行。
实际项目里最常见的自定义拦截器有这么几类:统一打印日志的、统一添加鉴权Token的、统一做请求加密的、统一统计耗时的。我分享一个最实用也最简单的日志拦截器写法:
class LoggingInterceptor : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val request = chain.request() val startTime = System.nanoTime() // 请求前打印 println("--> Sending request: ${request.method} ${request.url}") val response = chain.proceed(request) val duration = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime) // 响应后打印 println("<-- Received response for ${response.request.url} in ${duration}ms") println("<-- Response code: ${response.code}") return response } }拦截器顺序影响特别大,这一点我在项目里吃过亏。比如你要做统一的Token刷新逻辑,如果把它放在权限拦截器之后,可能请求已经被拦截器拦截了,Token还没刷新的问题就会出现。另一个常见坑是,如果在拦截器里调用了chain.proceed()之后又对响应做了修改,比如统一解密响应体,一定要记得Response是不可变的,需要用response.newBuilder()来构造新响应返回,否则直接改动会引发异常。
4. 连接池与连接复用:OkHttp高性能的底牌
4.1 为什么说一次TCP连接的开销远超你想象
一个HTTP请求真正传数据的时间往往很短,但建连的时间可能比传输本身还长。TCP三次握手需要一到两个RTT,如果走HTTPS,还要再加上TLS握手的一到两个RTT。RTT就是数据包从客户端到服务器的往返时间,按照移动网络平均50到100毫秒来算,一次新连接光是握手就得消耗上百毫秒。
所以连接复用的意义,就是把“每次请求都重新握手”的成本降到最低。第一次请求建立连接后,服务器和客户端都维持这条连接,后续请求直接复用,传输数据就快得多了。OkHttp内置的连接池就是这个思路的工程实现。
4.2 OkHttp的连接池是怎么设计出来的
连接池的核心类是ConnectionPool,内部维护了一个ConcurrentLinkedQueue来存放空闲连接。它有几个重要的默认参数:最大空闲连接数maxIdleConnections默认为5,空闲连接存活时间keepAliveDuration默认为5分钟。也就是说,一个连接空闲下来后最多在池子里待5分钟,超过时间就会被清理线程回收,避免长时间占着socket资源。
我记得这个参数在Android平台上被一些大厂调整过,因为不同厂商的网络栈优化策略不同,有的会把keepAliveDuration调短一些,减少服务器端维护无谓连接的压力。实际开发中如果不是特别了解业务场景,不建议乱调,默认值在绝大多数场景下表现都很稳定。
连接池还有一个专门的清理任务,它内部会周期性扫描池里的空闲连接,把超过存活时间或者超过最大数量的连接关闭掉。这个清理逻辑不是简单地“每隔X秒扫一次”,而是会根据最近一个空闲连接的时间动态计算下一次清理时机,尽可能减少无谓的唤醒操作。
4.3 路由机制:连接池怎么决定复用哪条连接
说到连接复用,就不得不提Route的概念。一个URL可能解析出多个IP地址,OkHttp会按顺序尝试连接这些地址,直到某个IP成功建立了连接。这个尝试的过程依赖于RouteSelector。
连接复用的一个关键前提是路由必须匹配。如果请求A走了IP1,请求B想复用同一条连接,那它对应的IP也得是IP1。用生活化的类比来解释,连接池就像租车公司的停车场,里面停着几辆车,但是每辆车只服务指定的几条路线。你要去的目的地正好有对应的车在,你才能直接开走,否则还得现场调度。
这个机制导致了在实际弱网环境中一个非常常见的现象:当网络从WiFi切到4G,或者域名解析结果发生变化后,原本可复用的连接突然失效了,OkHttp会重新发起连接。有些经验不足的开发者遇到这种情况会误以为是域名问题,实际上这是路由选择更新的正常表现。
5. 缓存策略:让OkHttp帮你省流量
5.1 HTTP缓存协议:客户端和服务端的“协商”
OkHttp的缓存完全遵循HTTP标准缓存协议,不是自己拍脑袋定的规则。这个协议说起来也不复杂,核心围绕两个问题:响应能不能缓存?缓存有效期有多长?
服务端通过响应头告诉客户端缓存策略,最常见的字段是Cache-Control。比如Cache-Control: max-age=86400表示这个响应可以被缓存1天,在1天内客户端再发同样请求可以直接用缓存。ETag和Last-Modified则是另一种玩法,它们不直接说“可以缓存”,而是提供一个“验证令牌”,客户端带上这个令牌发个条件请求,服务端发现内容没变就返回304,客户端继续用本地缓存;内容变了才返回200和新的响应体。
很多开发者只会在Retrofit或OkHttp的配置里加一句cache,就以为万事大吉了,其实这远远不够。服务端如果不设置任何缓存相关的响应头,OkHttp默认是不缓存响应体的。你配置了缓存目录,但服务端响应没有给出允许缓存的标识,结果还是每次请求都走网络。
5.2 OkHttp中如何开启和验证缓存
开启OkHttp缓存非常简单,只需要在构建客户端时提供一个指定大小的Cache对象:
val cache = Cache( File(context.cacheDir, "http_cache"), 50L * 1024 * 1024 // 50MB ) val client = OkHttpClient.Builder() .cache(cache) .build()这里有两个细节值得注意。第一,缓存目录必须是应用可读写的目录,context.cacheDir是最常见的选择,不要放到externalStorage这种需要权限的地方。第二,缓存大小不是越大越好,过大的缓存会让磁盘空间吃紧,而且OkHttp的缓存索引本身也要占用内存,一般控制在10MB到50MB之间比较合适。
想要验证缓存是否真的生效,可以用日志拦截器打印响应头。当OkHttp命中缓存时,返回的Response里会带一个特殊的头字段okhttp-cache,值为GET FROM CACHE或者类似标识。如果没有这个头字段,说明这次请求实际上走了网络。还有一个更直观的方式,用Charles或Replay这类抓包工具,看有没有发真实的网络请求。
5.3 缓存刷新和禁用的坑,我踩过
缓存带来的最典型问题就是“数据不新鲜”。比如一个App的首页接口设了很长的max-age,用户每次打开App看到的都是旧数据。要强制刷新,可以在请求头里加Cache-Control: no-cache,它的意思是“不要直接拿缓存,先去服务端验证一下”,服务端返回304就继续用缓存,返回200就更新缓存。
还有一类场景是,同一个接口需要区分缓存和不定制缓存,比如用户信息接口必须实时,而配置类接口可以缓存一小时。这个需求可以在OkHttp拦截器里动态判断URL,给特定请求追加缓存头。
我在实际项目里遇过一个特别诡异的问题:某个版本的App内嵌H5页面,图片总是显示旧版本。排查下来才发现,问题不在前端,而在后端返回的HTML和图片资源都没有设置正确的缓存头,OkHttp默认不缓存,导致每次加载全是全量请求,在弱网场景下自然会卡顿。后来和后端同学约定好静态资源统一设置Cache-Control,体验立竿见影。
6. 协议层面的能力:HTTP/2、HTTPS、WebSocket
6.1 HTTP/2多路复用:一个连接处理无数请求
HTTP/1.1的队头阻塞问题相信大家都有耳闻:同一个连接上,如果前一个请求迟迟没响应,后面的请求就得排队等着。HTTP/2通过多路复用解决了这个问题,它允许同一个TCP连接上同时交错传输多个请求和响应,每个请求被切分成一个个二进制帧,用流ID来区分归属。
OkHttp从很早就支持了HTTP/2。当客户端和服务端都支持HTTP/2时,OkHttp会优先使用HTTP/2协议,多个请求共享同一条连接,大幅降低连接数量。我在压测时观察过这个效果,同样一批请求,HTTP/1.1下可能要建立十几个连接,切到HTTP/2后只需要两三个连接就能跑完。
要查看当前连接用的什么协议,可以通过response.protocol字段获取,它会返回Protocol.HTTP_2或Protocol.HTTP_1_1。如果发现线上流量都是HTTP/1.1,大部分原因是服务端支持不到位,或者服务端网关关闭了HTTP/2,这种情况下可以考虑在CDN或负载均衡层开启HTTP/2支持。
6.2 HTTPS证书校验:安全性和开发便利性的权衡
默认情况下OkHttp的HTTPS校验是非常严格的,它会验证证书链、验证明文、验证域名。这也是它默认为安全起见把很多不合法证书直接拒掉的原因。
但开发中经常会遇到自签名证书或内网测试证书导致的握手失败。很多初学者图省事,直接写一个信任所有证书的TrustManager,把校验关掉。这种做法极其危险,等于把应用的大门敞开,任何中间人都能拦截你的数据,明文传输的内容相当于裸奔。我在代码评审时看到这种写法,一般都会要求打回。
正确的做法是把你的自签名证书导入到应用的res/raw目录,构建自定义的SSLSocketFactory时使用这个证书。这个过程略繁琐,但这个复杂性是必须承担的。生产环境的HTTPS配置更应该敬畏,证书锁定(Certificate Pinning)是一个更高级的方案,但要注意它有爆炸半径:如果服务端证书轮换,而App没及时更新锁定指纹,所有请求都会失败。
6.3 WebSocket长连接:用OkHttp实现实时通信
极其需要简单写一下,因为OkHttp提供了非常便捷的WebSocket支持。普通的WebSocket场景,比如IM消息推送、股票行情刷新,用OkHttp都很合适。
val request = Request.Builder() .url("wss://echo.websocket.org") .build() val listener = object : WebSocketListener() { override fun onOpen(webSocket: WebSocket, response: Response) { webSocket.send("Hello, WebSocket!") } override fun onMessage(webSocket: WebSocket, text: String) { println("收到消息: $text") } override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { println("WebSocket 连接失败: ${t.message}") } } val webSocket = client.newWebSocket(request, listener) client.dispatcher.executorService.shutdown()这里有个小坑必须提醒:上面的webSocket对象如果不保存引用,有可能被GC回收,导致连接莫名断开。所以实际使用中,应该把WebSocket对象保存在一个单例或长生命周期对象里。另外,WebSocket的心跳检测需要自己处理,OkHttp不会帮你自动发ping,如果你只是断线之后重新连接,网络层可能会因为长时间没有数据交换而被服务端或中间设备断开。
6.4 Retrofit、协程与OkHttp的黄金组合
现在真正写Android应用,很少有人直接面对OkHttp的Call接口,更多是通过Retrofit封装一层。Retrofit用了OkHttp作为底层平台,把接口定义变成了可调用的服务方法。协程普及之后,配合suspend函数,网络请求的写法变得极其优雅:
interface ApiService { @GET("user/info") suspend fun getUserInfo(@Query("id") userId: String): UserInfo } class UserRepository(private val api: ApiService) { suspend fun fetchUserInfo(userId: String): UserInfo { return api.getUserInfo(userId) } }Retrofit对OkHttp完全是“组合优于继承”的经典示范,你依然可以在构建Retrofit时把自定义的OkHttpClient实例传进去,拦截器、缓存、超时这些能力全部继承自OkHttp。在实际项目中,我的做法是把OkHttpClient做成一个全局单例,统一配置超时、日志、缓存、拦截器,不同业务模块共用这一个实例,避免每个模块各自创建客户端导致连接池浪费。
7. 常见问题与排查技巧实录
OkHttp虽然稳定,但实际项目里该踩的坑一个都少不了。我把这些年做Android网络层排查时遇到的高频问题整理成一张速查表,希望能帮你省点时间:
| 常见问题 | 典型现象 | 排查思路 | 解决方案 |
|---|---|---|---|
| CLEARTEXT通信被禁止 | 请求报错CLEARTEXT communication to xxx not permitted | Android 9之后默认禁止明文HTTP流量 | 使用HTTPS;或按需在networkSecurityConfig里允许特定域名明文 |
| 连接超时频繁 | SocketTimeoutException,接口偶尔慢 | 看是不是弱网,或者服务端响应慢 | 分段设置connectTimeout、readTimeout;区分读超时和连接超时 |
| 域名解析失败 | UnknownHostException,只在一个网络环境出现 | 换网络测试,抓日志看DNS解析是否正常 | 给OkHttp配置自定义DNS,做本地解析缓存或备用域名 |
| HTTPS证书校验失败 | 接口突然全部失败,报SSLHandshakeException | 看是否服务端证书过期或没配完整证书链 | 更新证书;客户端做证书锁定前确认轮换机制 |
| 上传大文件OOM | 上传视频或大图片时内存暴涨 | 默认RequestBody会把流读入内存 | 使用自定义RequestBody,用流式写入,避免整个文件进内存 |
| 缓存不生效 | 设置了cache但还是每次走网络 | 抓包看响应头有没有Cache-Control、ETag | 让服务端补充缓存响应头;确认响应码是可缓存的 |
我挑两个特别值得展开的说一下。
第一个是超时问题。很多同学以为设置了readTimeout就一定不会超时,其实这里的超时针对的是“两个数据包之间的间隔时间”,不是“整个请求的耗时”。如果服务端一直每隔几秒给你发一个包证明自己还活着,但整个请求拖了几分钟,readTimeout是不会触发的。所以如果需要控制整个请求的最长耗时,得自己用call.timeout()来实现。
第二个是上传大文件OOM。默认的RequestBody.create()会先把整个内容读取到内存再写入请求体,如果你上传一个2GB的文件,内存直接爆掉。这时候需要自定义RequestBody,自己实现writeTo()方法,用文件流边读边写。这是我在一个视频分享类App里实际踩过的坑,当时线上崩溃率突然飙升,定位到就是上传模块的OOM问题。
再分享一个排查工具经验:遇到OkHttp的异常,一定要先看完整的堆栈,再结合抓包工具对比请求链路。Android自带的adb shell抓包能力虽然有限,但通过Stetho这类工具也能在开发模式看到网络请求。如果是线上问题,建议在日志拦截器里把请求URL、响应码、耗时、异常信息都打印出来,排查效率会高非常多。
8. 我把OkHttp配置成什么样,才敢用于生产环境
最后分享一段我个人项目里落地的客户端配置,可以直接抄作业。这个配置覆盖了超时、缓存、日志、拦截器、错误上报这些生产必需的环节:
object NetworkManager { private val client: OkHttpClient by lazy { val cache = Cache( File(AppContext.get().cacheDir, "http_cache"), 30L * 1024 * 1024 ) val loggingInterceptor = HttpLoggingInterceptor().apply { level = if (BuildConfig.DEBUG) { HttpLoggingInterceptor.Level.BODY } else { HttpLoggingInterceptor.Level.NONE } } OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(20, TimeUnit.SECONDS) .writeTimeout(20, TimeUnit.SECONDS) .cache(cache) .retryOnConnectionFailure(true) .addInterceptor(AuthInterceptor()) // 统一加Token .addInterceptor(loggingInterceptor) // 日志打印 .addInterceptor(ErrorReportInterceptor()) // 异常上报 .connectionPool(ConnectionPool(5, 5, TimeUnit.MINUTES)) .pingInterval(20, TimeUnit.SECONDS) // WebSocket心跳间隔 .build() } fun getClient(): OkHttpClient = client }这里要特别说下pingInterval,它不只对WebSocket生效,对HTTP/2连接也有保活作用。加上之后可以保持长期连接不被中间设备断开,但注意不要在每台机器上都开启,有少数服务端不支持ping帧的兼容问题。
根据我个人的经验,OkHttp已经是Android生态里网络层最成熟可靠的选择,真正的问题往往出在使用姿势,而不是框架本身。多花点时间理解它的连接池、缓存和拦截器机制,比盲目追新工具更有价值。如果你在排查网络问题时始终不得要领,不妨先从OkHttp的请求日志开始,一层一层剥开看,很多时候答案就在那条链路的每一个拦截器里。