OkHttp 基础:请求与响应,你真的搞懂了吗?

聊协程泄漏前,咱们先把OkHttp最核心的基础夯实。别小看请求和响应这俩环节,我见过不少线上问题,根子其实就埋在「发请求」和「收响应」的细节里。

OkHttp 说白了就是个 HTTP 客户端。你给它一个 URL,它帮你把网络请求发出去,再把服务器返回的数据装进 Response 对象送回来。听起来简单吧?但实际用起来,坑还真不少。

核心认知: OkHttp 的请求与响应是「同步」的底层机制,但上层可以用 Call 接口包装成异步。理解这一点,对后续协程封装至关重要。

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 压力蹭蹭涨。

避坑提醒: 如果有大量相同 URL 的请求,考虑复用 Request 对象,或者用 Factory 模式缓存 Builder 实例。

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 块没执行。

最佳实践: 使用 try-finally 或 Kotlin 的 use 函数确保 Response 被关闭。
// 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 从构建请求到处理响应的核心节点串了起来。建议你把它当作战术地图,排查问题时照着看卡在哪一步。

OkHttp 请求与响应核心流程 构建 Request client.newCall(request) 同步 execute() 异步 enqueue() 接收 Response 关闭 body / use 关键:Response.body().string() 只能调用一次,因为底层流读完就关闭了。需要多次读取请先转为 byte[] 或缓存。

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,可能会在已被取消的协程里继续执行,造成非预期的状态。

我曾经踩过的坑: 在一次列表下拉刷新中,用户快速连续触发刷新,旧的协程取消了,但 onResponse 里返回的数据依然更新了 RecyclerView,导致 UI 闪烁。后来加了 isActive 检查才解决。

所以,协程里的 OkHttp 请求,一定要处理取消信号。这是后续排查协程泄漏的突破口之一。

6. 小结:一张表记住关键点

维度要点常见问题
Request 构建不可变、线程安全循环内重复 build 浪费内存
Call 执行同步需子线程,异步自动线程池主线程同步导致 ANR
Response 关闭使用 use 或 try-finally未关闭导致连接泄漏
协程适配suspendCancellableCoroutine忽略取消信号导致状态异常
取消机制cancel() 会关闭底层 socket服务端可能收到 RST 需处理