OkHttp 基础:请求与响应,你真的搞懂了吗?
聊协程泄漏前,咱们先把OkHttp最核心的基础夯实。别小看请求和响应这俩环节,我见过不少线上问题,根子其实就埋在「发请求」和「收响应」的细节里。
OkHttp 说白了就是个 HTTP 客户端。你给它一个 URL,它帮你把网络请求发出去,再把服务器返回的数据装进 Response 对象送回来。听起来简单吧?但实际用起来,坑还真不少。
1. 构建一个请求——你的第一行代码
我习惯先把 Request 对象拆成三部分:URL、请求头、请求体。说白了,就是告诉服务器「我要什么、怎么要、带什么过去」。
// 最简单的 GET 请求
Request getRequest = new Request.Builder()
.url("https://api.example.com/users")
.header("Authorization", "Bearer xxx")
.build();
// 带 body 的 POST 请求
Request postRequest = new Request.Builder()
.url("https://api.example.com/users")
.post(RequestBody.create(
MediaType.parse("application/json"),
"{\"name\":\"张三\"}"
))
.build();
看到没?Builder 模式用起来很舒服。但你要注意一点——每次调用 .build() 都会生成新对象。这算是 OkHttp 的设计哲学:不可变请求,线程安全。我曾经在循环里重复 build 同一个 URL 的请求,结果发现创建了上百个 Request 对象,GC 压力蹭蹭涨。
2. 发起请求——同步还是异步?
OkHttp 的 Call 对象是对一次请求-响应过程的抽象。你可以通过 OkHttpClient 拿到 Call 实例,然后决定怎么执行。
我个人建议:Android 上永远不要在 UI 线程执行同步请求。为什么?因为 OkHttp 默认同步是阻塞的,你这样做就是在挑战 ANR 的底线。
| 方式 | 方法 | 线程要求 | 典型场景 |
|---|---|---|---|
| 同步 | call.execute() | 必须子线程 | 后台批量下载、定时任务 |
| 异步 | call.enqueue(callback) | 自动切回回调线程 | UI 交互、普通网络请求 |
// 同步(务必在子线程)
new Thread(() -> {
try {
Response response = call.execute();
// 处理响应
} catch (IOException e) {
e.printStackTrace();
}
}).start();
// 异步(自动在 Dispatcher 线程池执行)
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
// 请求失败
}
@Override
public void onResponse(Call call, Response response) throws IOException {
// 请求成功,注意 response.body() 是流式读取
}
});
3. 处理响应——千万别忘了 close
响应对象拿到手,很多人直接 response.body().string() 就完事了。这里有个大坑:Response.body() 返回的是 ResponseBody,它持有底层 IO 流。如果你不关掉它,连接池里的连接不会被回收,最终导致 socket 泄漏。
我记得有个项目,上线后 OOM 频发,最后定位到是几百个请求的 Response 没有被 close。OK,当时排查了半天,发现是同事在 onResponse 里直接抛了异常,导致 finally 块没执行。
// Java 写法
try (Response response = call.execute()) {
String body = response.body().string();
// 使用 body
} catch (IOException e) {
e.printStackTrace();
}
// Kotlin 写法
client.newCall(request).execute().use { response ->
val body = response.body?.string()
// 使用 body
}
容易混淆的是:异步回调里也需要手动 close。OkHttp 不会帮你自动关闭,回调返回后 Response 对象就属于你了,你得负责清理。
4. 请求流程全景图
下面这张图把 OkHttp 从构建请求到处理响应的核心节点串了起来。建议你把它当作战术地图,排查问题时照着看卡在哪一步。
5. 协程场景下的请求封装
有了上面的基础,我们再看看 Kotlin 协程怎么用 OkHttp。核心思路:把 Call 的异步回调适配成挂起函数。我常用扩展函数实现:
suspend fun Call.await(): Response = suspendCancellableCoroutine { continuation ->
enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
// 协程取消时,自动取消网络请求
if (continuation.isCancelled) return
continuation.resumeWithException(e)
}
override fun onResponse(call: Call, response: Response) {
if (continuation.isCancelled) return
continuation.resume(response)
}
})
// 协程取消时自动取消 Call
continuation.invokeOnCancellation {
if (!isCanceled()) cancel()
}
}
你看,这个 await() 函数把 Callback 世界和协程世界桥接起来了。但有个隐藏问题:如果协程被取消,而网络请求已经发出,服务端可能还在处理。这会导致什么?连接释放了,但 socket 没关闭?不对,cancel() 会关闭底层连接,服务端会收到 RST。但如果你在 onResponse 里没有判断 continuation.isCancelled,可能会在已被取消的协程里继续执行,造成非预期的状态。
所以,协程里的 OkHttp 请求,一定要处理取消信号。这是后续排查协程泄漏的突破口之一。
6. 小结:一张表记住关键点
| 维度 | 要点 | 常见问题 |
|---|---|---|
| Request 构建 | 不可变、线程安全 | 循环内重复 build 浪费内存 |
| Call 执行 | 同步需子线程,异步自动线程池 | 主线程同步导致 ANR |
| Response 关闭 | 使用 use 或 try-finally | 未关闭导致连接泄漏 |
| 协程适配 | suspendCancellableCoroutine | 忽略取消信号导致状态异常 |
| 取消机制 | cancel() 会关闭底层 socket | 服务端可能收到 RST 需处理 |