1. 从HttpURLConnection到OkHttp:为什么我们需要一个更好的网络库?
如果你在Android开发领域待过几年,肯定对HttpURLConnection这个名字不陌生。早期的Android网络编程,它几乎是唯一的选择。我还记得那时候,为了发起一个简单的POST请求,需要写几十行样板代码:手动管理连接、处理输入输出流、拼接请求体、处理重定向和超时。更别提多线程环境下的连接管理和异常处理了,稍有不慎就是内存泄漏或者连接池耗尽。那时候,一个网络请求的代码块,看起来就像一座摇摇欲坠的积木塔,功能实现了,但没人敢说它稳固。
后来,Apache的HttpClient一度成为更强大的替代品,但Android团队在后续版本中逐渐弃用了它,并推荐使用改进后的HttpURLConnection。虽然有所改善,但核心问题没变:API设计过于底层和繁琐,缺乏现代应用开发所需的高级特性。比如,没有内置的响应缓存、没有透明的GZIP压缩、没有连接池的精细化管理,更不用说拦截器、WebSocket这类高级功能了。开发者需要自己造轮子,或者引入第三方封装,导致项目中的网络层代码五花八门,维护成本极高。
OkHttp的出现,彻底改变了这个局面。它不是对HttpURLConnection的简单封装,而是一个从零设计的、支持HTTP/2和QUIC协议的现代化客户端。它的设计哲学是:高效、可靠、易用。我第一次接触OkHttp时,最直观的感受是,原来一个网络请求可以如此简洁:三行代码,一个异步回调,请求就发出去了。但它的强大远不止于此。OkHttp在底层默默做了大量优化工作:连接复用、透明压缩、响应缓存、自动重试、域名系统(DNS)优化等等。这些特性对于提升App的网络性能、降低耗电量、改善用户体验至关重要,而开发者几乎无需关心其实现细节。
举个例子,在没有连接复用的时代,每次请求都可能经历TCP三次握手、TLS握手(如果是HTTPS)的过程,延迟很高。OkHttp的连接池机制,使得对同一主机的多个请求可以共享同一个Socket连接,极大地减少了延迟和系统开销。再比如,它的拦截器(Interceptor)机制,提供了一种强大而灵活的方式,让你可以在请求发出前和响应返回后插入自定义逻辑,统一添加Header、打印日志、修改请求体、重试请求等操作变得异常简单。
所以,当我们谈论“理解和使用OkHttp”时,我们不仅仅是在学习一个库的API调用。我们是在学习如何构建一个健壮、高效、可维护的现代Android网络层。无论你是想快速上手实现基本功能,还是想深入底层原理进行高级定制,OkHttp都是一个绕不开的核心课题。接下来,我会从实际项目经验出发,带你由浅入深地拆解OkHttp。
2. OkHttp核心架构与关键组件拆解
要真正用好OkHttp,不能只停留在OkHttpClient.newCall(request).execute()这个层面。理解其内部的核心组件和它们之间的协作关系,是解决复杂问题和进行高级定制的基础。OkHttp的架构可以看作一个精心设计的管道系统,请求和响应在这个管道中流动,经过各个组件的处理。
2.1 OkHttpClient:全局的配置与调度中心
OkHttpClient是你的网络客户端单例,它持有所有全局配置。最佳实践是,对于一个App,通常应该复用同一个OkHttpClient实例。因为每个客户端实例都维护着自己独立的连接池、线程池和缓存。创建多个实例会导致资源浪费,失去连接复用的优势。
它的配置项非常丰富,涵盖了网络行为的方方面面:
- 超时控制:
connectTimeout,readTimeout,writeTimeout,callTimeout。这里有个容易混淆的点:callTimeout是整个调用(包括DNS解析、连接、写入请求体、读取响应头、读取响应体)的总超时,而其他是针对特定阶段的超时。 - 拦截器:通过
addInterceptor()和addNetworkInterceptor()添加。这是OkHttp最强大的特性之一,后文会详细展开。 - 缓存:通过
cache()方法设置一个Cache实例,用于HTTP响应缓存。OkHttp的缓存完全遵循HTTP协议规范,可以帮你处理Cache-Control、ETag、Last-Modified等头部,实现高效的离线或弱网体验。 - 连接池:
connectionPool控制着Socket连接的复用策略。默认的连接池会保持最多5个空闲连接,存活时间为5分钟。你可以根据服务器特性和应用场景调整这些参数。 - 协议:
protocols指定支持的协议列表,如HTTP/2、HTTP/1.1。默认包含HTTP/2,这能带来多路复用、头部压缩等好处。 - DNS:通过
dns()接口可以自定义DNS解析策略。默认使用系统的InetAddress.getAllByName()。你可以在这里集成自己的DNS服务,比如实现HTTPDNS来绕过运营商Local DNS劫持,或者对特定域名返回硬编码的IP地址进行故障演练。
一个配置良好的OkHttpClient可能长这样:
val okHttpClient = OkHttpClient.Builder() .connectTimeout(15, TimeUnit.SECONDS) // 连接超时 .readTimeout(30, TimeUnit.SECONDS) // 读取超时 .writeTimeout(30, TimeUnit.SECONDS) // 写入超时 .callTimeout(60, TimeUnit.SECONDS) // 整个调用超时 .retryOnConnectionFailure(true) // 自动重试连接失败 .cache(Cache(File(context.cacheDir, "http_cache"), 50 * 1024 * 1024L)) // 50MB缓存 .addInterceptor(LoggingInterceptor()) // 应用拦截器:日志 .addInterceptor(AuthInterceptor()) // 应用拦截器:统一添加Token .addNetworkInterceptor(StethoInterceptor()) // 网络拦截器:调试 .connectionPool(ConnectionPool(10, 5, TimeUnit.MINUTES)) // 自定义连接池 .build()2.2 Request与Response:不可变的数据载体
Request和Response都是不可变(Immutable)对象。这意味着一旦创建,就不能被修改。这种设计保证了线程安全,并且便于拦截器链进行安全的变换操作。如果你想修改一个请求,必须通过它的newBuilder()方法创建一个新的Builder,修改后再构建新的Request。
Request的核心构成:
- url: 请求地址。注意,OkHttp会解析URL并规范化(比如编码特殊字符)。
- method: 请求方法,如
GET、POST、PUT等。 - headers: 请求头。使用
Headers.Builder或headersOf()创建。OkHttp会自动添加一些必要的头,如Host、User-Agent(可覆盖)、Connection等。 - body: 请求体。对于
POST、PUT等方法,需要构建RequestBody。OkHttp提供了多种便捷的RequestBody子类,如FormBody(表单)、MultipartBody(多部分表单,用于文件上传)、RequestBody.create()(用于JSON等媒体类型)。
Response的核心构成:
- request: 产生这个响应的原始请求。
- protocol: 使用的协议(如
HTTP/1.1,h2)。 - code: HTTP状态码(200, 404等)。
- message: 状态消息(OK, Not Found)。
- headers: 响应头。
- body: 响应体,是一个
ResponseBody对象。这里有一个至关重要的点:响应体必须被关闭,否则会导致连接泄漏和内存泄漏。无论你是消费了响应内容,还是发生异常,都必须确保body.close()被调用。使用use扩展函数(Kotlin)或try-with-resources(Java)是推荐做法。
2.3 Call:一次请求的生命周期控制器
Call接口代表一个准备好执行的请求(由OkHttpClient.newCall(request)创建)。它是对一次HTTP请求的抽象封装。你可以同步执行(call.execute(),阻塞当前线程直到收到响应)或异步执行(call.enqueue(callback),在后台线程执行并通过回调通知结果)。
每个Call只能执行一次。尝试再次执行会抛出IllegalStateException。你可以通过call.cancel()随时取消一个正在进行的请求,这对于列表快速滑动时取消不必要的图片加载等场景非常有用。
RealCall是Call接口在OkHttp内部的唯一实现。它负责协调拦截器链、连接管理、重试等核心流程。当你调用execute()或enqueue()时,RealCall会启动整个引擎。
2.4 拦截器链:OkHttp的“灵魂”
这是OkHttp最精妙的设计。你可以把一次HTTP请求想象成一颗子弹,拦截器链就是这颗子弹穿过的一系列处理层。RealCall的getResponseWithInterceptorChain()方法是核心。链中的拦截器按顺序分为两大类:
应用拦截器(Application Interceptors):通过
addInterceptor()添加。它们处于链的最外层,最早被调用,最晚收到响应。它们看到的Request是最原始的,看到的Response是最终的。适合处理与具体业务逻辑无关的、应用层面的操作,例如:- 统一添加认证Token。
- 记录请求和响应的完整日志(包括Body)。
- 全局请求参数注入。
- 基于响应的统一错误处理(如Token过期,跳转登录)。
网络拦截器(Network Interceptors):通过
addNetworkInterceptor()添加。它们处于链的更深处,在重定向和重试之后被调用。它们看到的Request是经过了应用拦截器处理、并且添加了必要网络头(如Content-Length,Transfer-Encoding)的版本;看到的Response是包含网络层信息(如接收到的原始Header)的版本。适合处理需要感知网络层细节的操作,例如:- 修改请求头信息(但需注意不要破坏HTTP协议)。
- 监控网络流量和数据消耗。
- 与Stetho、Charles等调试代理工具集成。
链的中间是OkHttp内置的一系列必须的拦截器,按顺序包括:RetryAndFollowUpInterceptor(处理重试和重定向)、BridgeInterceptor(补全用户请求缺失的HTTP头、处理GZIP压缩)、CacheInterceptor(处理缓存)、ConnectInterceptor(建立连接)、CallServerInterceptor(向服务器读写数据)。
理解这个链条的顺序至关重要。例如,如果你在应用拦截器里修改了请求URL,那么缓存拦截器将基于修改后的URL进行缓存查找和存储。而如果你在网络拦截器里修改了URL,则缓存拦截器看到的是原始的URL。
3. 实战:从基础请求到高级应用
理解了核心架构,我们来看看如何在项目中实际使用OkHttp。我会从最简单的GET请求开始,逐步深入到文件上传、配置管理等复杂场景。
3.1 发起你的第一个请求:同步与异步
同步请求简单直接,但会阻塞调用线程,因此绝对不能在Android的主线程(UI线程)上执行,否则会引发NetworkOnMainThreadException导致应用崩溃。它适用于后台任务或工作线程。
// 在IO线程或协程的IO Dispatcher中执行 val client = OkHttpClient() val request = Request.Builder() .url("https://api.example.com/data") .build() try { val response = client.newCall(request).execute() // 同步执行,阻塞 response.use { // 使用use确保ResponseBody被关闭 if (response.isSuccessful) { val responseBodyString = response.body?.string() // 注意:string()只能调用一次 println("Response: $responseBodyString") } else { println("Request failed: ${response.code} - ${response.message}") } } } catch (e: IOException) { println("Request failed: ${e.message}") }异步请求是Android开发中的主流方式。它不会阻塞调用线程,通过回调在后台线程处理结果。
val client = OkHttpClient() val request = Request.Builder() .url("https://api.example.com/data") .build() client.newCall(request).enqueue(object : Callback { override fun onFailure(call: Call, e: IOException) { // 网络错误、超时、取消等 e.printStackTrace() // 注意:回调默认在OkHttp内置的Dispatcher线程池执行,不是主线程! runOnUiThread { // 需要切回主线程更新UI showError("Network error: ${e.message}") } } override fun onResponse(call: Call, response: Response) { // 收到HTTP响应(即使状态码是404、500,也会走到这里) response.use { if (response.isSuccessful) { val responseBodyString = response.body?.string() runOnUiThread { updateUI(responseBodyString) } } else { runOnUiThread { showError("Server error: ${response.code}") } } } } })注意:
Response.body()?.string()方法会将整个响应体读入内存并转换为字符串。对于大文件(如图片、视频),这会消耗大量内存。对于这类情况,应该使用Response.body()?.byteStream()或Response.body()?.source()来以流的方式处理。
3.2 构建复杂的请求:POST、表单、文件上传、JSON
1. POST表单数据:
val formBody = FormBody.Builder() .add("username", "zhangsan") .add("password", "123456") .build() val request = Request.Builder() .url("https://api.example.com/login") .post(formBody) // 指定方法为POST,并传入RequestBody .build()2. POST JSON数据:
val mediaType = "application/json; charset=utf-8".toMediaType() val json = """{"title": "Hello", "body": "World", "userId": 1}""" val requestBody = RequestBody.create(mediaType, json) val request = Request.Builder() .url("https://jsonplaceholder.typicode.com/posts") .post(requestBody) .addHeader("Custom-Header", "MyValue") // 添加自定义Header .build()3. 多部分表单文件上传: 这是上传文件(如图片、文档)的标准方式。MultipartBody可以混合多种类型的数据部分。
val file = File("/sdcard/photo.jpg") val requestBody = MultipartBody.Builder() .setType(MultipartBody.FORM) .addFormDataPart("title", "My Photo") .addFormDataPart("description", "A picture from my trip") .addFormDataPart( "image", // 字段名 file.name, // 文件名 file.asRequestBody("image/jpeg".toMediaType()) // 文件体和MIME类型 ) .build() val request = Request.Builder() .url("https://api.example.com/upload") .post(requestBody) .build()3.3 配置管理:如何组织你的OkHttpClient
在真实项目中,你很少会直接使用OkHttpClient()的无参构造函数。不同的业务场景可能需要不同的客户端配置。常见的组织方式有:
- 全局单例客户端:用于大多数通用API请求,配置了缓存、日志、超时等基础设置。
- 特定用途客户端:
- 图片加载客户端:可能设置更短的超时、不同的缓存策略(只缓存到内存或磁盘)、禁用重试。
- 文件下载客户端:设置更长的读写超时,禁用响应缓存(避免占用过大磁盘空间),并添加进度监听拦截器。
- 长连接/WebSocket客户端:配置特定的ping间隔和协议。
我通常会在一个单例类或依赖注入(如Hilt/Dagger)的Module中管理这些客户端实例。
object NetworkModule { // 通用API客户端 val apiClient: OkHttpClient by lazy { OkHttpClient.Builder() .applyDefaultConfigs() .addInterceptor(AuthInterceptor()) .addInterceptor(LoggingInterceptor.create(Level.BODY)) .cache(provideCache()) .build() } // 图片加载客户端(例如给Glide/Picasso使用) val imageClient: OkHttpClient by lazy { OkHttpClient.Builder() .applyDefaultConfigs() .readTimeout(10, TimeUnit.SECONDS) // 图片加载可以快一点 .cache(Cache(File(context.cacheDir, "image_cache"), 100 * 1024 * 1024L)) // 100MB图片缓存 .build() } // 扩展函数:应用默认配置 private fun OkHttpClient.Builder.applyDefaultConfigs(): OkHttpClient.Builder { return this.connectTimeout(15, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .writeTimeout(30, TimeUnit.SECONDS) .retryOnConnectionFailure(true) .connectionPool(ConnectionPool(10, 5, TimeUnit.MINUTES)) } }4. 深入拦截器:打造可观测、可控制的网络层
拦截器是OkHttp的超级武器。通过自定义拦截器,你可以无侵入地增强整个网络层的能力。下面分享几个实战中高频使用的拦截器案例。
4.1 日志拦截器:洞察网络流量
虽然OkHttp官方提供了一个HttpLoggingInterceptor,但自己实现一个能让你更理解其原理,并可以定制输出格式(如输出到文件用于线上问题排查)。
class SimpleLoggingInterceptor : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val request = chain.request() val startNs = System.nanoTime() // 打印请求信息 println("--> ${request.method} ${request.url}") request.headers.forEach { name, value -> println("$name: $value") } request.body?.let { body -> val buffer = Buffer() body.writeTo(buffer) println("Request Body: ${buffer.readUtf8()}") } // 执行请求 val response: Response try { response = chain.proceed(request) } catch (e: Exception) { println("<-- HTTP FAILED: $e") throw e } // 打印响应信息 val tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs) println("<-- ${response.code} ${response.message} ${response.request.url} (${tookMs}ms)") response.headers.forEach { name, value -> println("$name: $value") } // 注意:这里不能直接调用response.body()?.string(),因为会消耗流,导致调用方读不到数据。 // 正确的做法是peek一下响应体,或者复制一份响应。 val responseBody = response.body val source = responseBody?.source() source?.request(Long.MAX_VALUE) // 缓冲整个响应体 val buffer = source?.buffer?.clone() val responseBodyString = buffer?.readUtf8() println("Response Body: $responseBodyString") // 返回原始的response,因为body的流没有被真正消耗 return response } }重要提示:在生产环境的日志拦截器中,要避免在非Debug模式下打印敏感的请求/响应体(如密码、Token),并且要注意日志级别控制,防止日志泛滥。
4.2 认证拦截器:自动管理Token
这是一个典型的应用拦截器,用于在请求头中自动添加认证Token,并在Token过期时尝试刷新。
class AuthInterceptor(private val tokenManager: TokenManager) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val originalRequest = chain.request() // 1. 尝试获取当前Token val token = tokenManager.getAccessToken() val requestWithToken = if (token != null) { originalRequest.newBuilder() .header("Authorization", "Bearer $token") .build() } else { originalRequest } // 2. 发起请求 var response = chain.proceed(requestWithToken) // 3. 检查响应码,如果是401未授权,尝试刷新Token并重试 if (response.code == 401) { synchronized(this) { // 防止并发刷新 val newToken = tokenManager.refreshAccessToken() // 同步刷新Token if (newToken != null && newToken != token) { // 刷新成功且Token已变 // 用新Token构建新请求 val newRequest = originalRequest.newBuilder() .header("Authorization", "Bearer $newToken") .build() // 关闭旧的响应体 response.close() // 用新请求重新执行 response = chain.proceed(newRequest) } else { // 刷新失败,可以跳转到登录页面 tokenManager.clearToken() // 这里可以抛出自定义异常,在onFailure中处理登录逻辑 } } } return response } }这个拦截器处理了常见的“静默刷新”场景。更复杂的实现可能还需要处理刷新Token时的并发请求排队、刷新失败后的重试策略等。
4.3 网络状态拦截器:无网时快速失败
在移动端,网络状态是不稳定的。我们可以在发起请求前先检查网络连接,如果没有网络,则直接抛出自定义异常,避免无谓的等待和资源消耗。
class NetworkStatusInterceptor(private val context: Context) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { if (!isNetworkAvailable(context)) { throw NoNetworkException() // 自定义异常 } return chain.proceed(chain.request()) } private fun isNetworkAvailable(context: Context): Boolean { val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val activeNetwork = connectivityManager.activeNetwork val capabilities = connectivityManager.getNetworkCapabilities(activeNetwork) return capabilities != null && (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) || capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) || capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) } } class NoNetworkException : IOException("No network connection available")将这个拦截器添加到客户端后,所有请求都会先经过网络检查。在Callback.onFailure中捕获NoNetworkException,就可以给用户展示友好的“网络不可用”提示。
5. 性能调优与疑难杂症排查
OkHttp开箱即用性能就不错,但在高并发、弱网络等复杂场景下,一些深入的调优和问题排查技巧能让你事半功倍。
5.1 连接池与超时优化
连接池(ConnectionPool)是复用HTTP/1.1连接或HTTP/2流的关键。默认配置(最大5个空闲连接,存活5分钟)对大多数应用是足够的。但在以下场景可能需要调整:
- 高频请求少量主机:可以适当增加
maxIdleConnections(比如10-20),让更多连接保持活跃,减少握手开销。 - 请求大量不同主机:默认池子可能不够用,但增加池大小要谨慎,因为每个空闲连接都占用文件描述符和内存。更重要的是确保及时清理。
- keepAliveDuration:默认5分钟。如果你的服务器连接保持时间更短,可以调低此值,避免使用已失效的连接。如果服务器支持长连接,可以适当调高。
超时设置需要根据业务类型区分:
- 普通API请求:连接超时15-30秒,读写超时30-60秒是比较常见的设置。
- 文件上传/下载:写超时和读超时应大幅延长,特别是对于大文件,可能需要几分钟。
- 实时性要求高的请求(如搜索建议):所有超时都应设置得很短(如2-5秒),并配合快速失败和重试策略。
callTimeout:这是整个请求的“总闸”,应设置为略大于connectTimeout + readTimeout + writeTimeout之和,作为一个安全网,防止请求无限期挂起。
5.2 缓存策略与离线体验
OkHttp的缓存非常强大,但需要正确配置请求和响应头才能生效。服务器必须在响应中提供有效的缓存控制头,如Cache-Control: max-age=3600(缓存1小时)或ETag、Last-Modified。
客户端也可以强制缓存策略:
val request = Request.Builder() .url(url) .cacheControl(CacheControl.FORCE_CACHE) // 强制使用缓存,即使过期(用于离线) .build() // 或者 val request = Request.Builder() .url(url) .cacheControl(CacheControl.Builder().maxAge(0, TimeUnit.SECONDS).build()) // 禁止缓存,总是请求网络 .build()一个常见的优化是:对于列表数据等非实时性要求极高的请求,可以设置较短的max-age(如60秒),这样在频繁下拉刷新时,既能快速展示缓存内容,又能在后台更新数据。
5.3 常见问题排查指南
问题一:应用在后台或锁屏后网络请求失败或变慢。
- 可能原因:Android系统的Doze模式和App Standby会限制网络访问。在Android 6.0以上,当设备空闲时,网络访问会被推迟。
- 排查与解决:
- 使用
adb shell dumpsys deviceidle命令查看设备空闲状态。 - 对于必须保活的后台服务,考虑使用
ForegroundService并获取FOREGROUND_SERVICE权限。 - 对于可以延迟的任务,使用
WorkManager,它会自动处理Doze模式下的执行窗口。 - 在测试时,可以通过
adb shell dumpsys battery unplug和adb shell am set-inactive <packageName> true来模拟Doze状态进行验证。
- 使用
问题二:偶现的SocketTimeoutException: timeout或ConnectException: Failed to connect to...。
- 可能原因:网络抖动、DNS解析失败、服务器不稳定、IPv6兼容性问题(部分网络环境下IPv6地址超时)。
- 排查步骤:
- 抓包分析:使用Charles、Fiddler或OkHttp的日志拦截器,查看请求是否发出、服务器是否响应、耗时在哪一步。
- 检查超时设置:是否设置过短?针对特定API是否需要单独调整?
- DNS问题:尝试在拦截器中打印最终连接的IP地址,看是否是DNS解析到了不可达的IP。可以考虑集成HTTPDNS。
- IPv6问题:这是一个隐藏较深的问题。如果服务器双栈支持不佳,客户端可能先尝试IPv6连接,超时后再回退到IPv4,导致整体超时。可以通过在
OkHttpClient.Builder中强制指定协议列表来暂时禁用IPv6测试:.protocols(listOf(Protocol.HTTP_1_1))。但这只是权宜之计,根本解决需要服务器端完善IPv6支持。
问题三:内存泄漏,特别是Call或ResponseBody未关闭。
- 症状:LeakCanary报告
RealCall或相关对象泄漏,或应用内存使用持续增长。 - 根因:
ResponseBody必须关闭以释放底层资源(Socket连接会回到连接池)。即使你不读取响应体,或者发生异常,也必须关闭。 - 强制检查:
- 对于同步调用,使用
response.use { }(Kotlin)或try-with-resources(Java)。 - 对于异步调用,在
Callback.onResponse和onFailure中,确保最终调用response.close()。最简单的做法就是在onResponse开头使用response.use { }。 - 在开发阶段,可以添加一个自定义的
Interceptor,在Response被包装时记录,并在最终关闭时检查,如果发现未关闭就打印警告或抛出异常(仅限Debug模式)。
- 对于同步调用,使用
问题四:CanceledException频繁出现。
- 可能原因:请求被主动取消(
call.cancel()),这是正常行为。常见于列表快速滑动时,图片加载库(如Glide)会取消离开屏幕的图片请求。 - 排查:检查代码中是否在页面销毁(如
onDestroy)时取消了所有未完成的请求。这是一个好习惯,可以避免不必要的网络消耗和回调更新已销毁的UI。确保你的Callback能正确处理取消异常,避免崩溃。
6. 与协程及现代架构的融合
在现代Android开发中,我们很少直接裸用Callback。Kotlin协程和Jetpack架构组件(如ViewModel、LiveData/Flow)已成为标准。OkHttp可以很好地与它们集成。
6.1 使用协程封装异步请求
我们可以利用suspendCancellableCoroutine将OkHttp的Callback模式转换为挂起函数,使其可以在协程中同步调用。
suspend fun OkHttpClient.executeCall(request: Request): Response { return suspendCancellableCoroutine { continuation -> val call = newCall(request) // 在协程被取消时,取消网络请求 continuation.invokeOnCancellation { call.cancel() } call.enqueue(object : Callback { override fun onFailure(call: Call, e: IOException) { if (!call.isCanceled()) { // 忽略因协程取消导致的失败 continuation.resumeWithException(e) } } override fun onResponse(call: Call, response: Response) { continuation.resume(response) } }) } } // 在ViewModel或Repository中使用 class MyViewModel(private val apiService: ApiService) : ViewModel() { fun fetchData() { viewModelScope.launch { try { val response = withContext(Dispatchers.IO) { okHttpClient.executeCall(request) } response.use { if (response.isSuccessful) { val data = parseResponse(response.body?.string()) _uiState.value = UiState.Success(data) } else { _uiState.value = UiState.Error("Server error: ${response.code}") } } } catch (e: IOException) { _uiState.value = UiState.Error("Network error: ${e.message}") } catch (e: CancellationException) { // 协程被取消,忽略 } } } }6.2 使用Retrofit简化API定义
对于RESTful API,强烈推荐使用Retrofit。它是一个类型安全的HTTP客户端库,底层默认使用OkHttp。它让你通过接口定义API,极大地简化了代码。
// 1. 定义API接口 interface GitHubService { @GET("users/{user}/repos") suspend fun listRepos(@Path("user") user: String): List<Repo> @POST("user/create") @Headers("Content-Type: application/json") suspend fun createUser(@Body user: User): Response<User> } // 2. 创建Retrofit实例(背后是OkHttpClient) val retrofit = Retrofit.Builder() .baseUrl("https://api.github.com/") .client(yourOkHttpClient) // 注入配置好的OkHttpClient .addConverterFactory(GsonConverterFactory.create()) .build() // 3. 创建服务对象并调用 val service = retrofit.create(GitHubService::class.java) viewModelScope.launch { try { val repos = service.listRepos("square") // 直接是同步调用,但在协程内是挂起的 _repos.value = repos } catch (e: Exception) { // 处理异常 } }Retrofit处理了所有请求构建、响应解析和线程切换的样板代码,让你专注于业务逻辑。你仍然可以通过配置底层的OkHttpClient来使用前面提到的所有高级功能(拦截器、缓存等)。
我个人在实际项目中的体会是,OkHttp + Retrofit + Kotlin协程是目前Android网络层最优雅、最高效的组合。OkHttp提供强大稳定的底层引擎,Retrofit提供简洁的API层,协程提供线性的异步编程体验。掌握好这三者,你就能应对绝大多数移动网络编程的挑战。最后再分享一个小技巧:在开发调试阶段,一定要充分利用拦截器来打印日志和配合抓包工具,这是定位网络问题最快的方式;而在线上环境,则要收敛日志级别,并考虑添加监控拦截器,将请求耗时、失败率等指标上报到你的APM系统,以便持续优化用户体验。