同步与异步请求:你的第一个选择
用了 OkHttp,第一个要面对的问题就是:到底用同步还是异步?
我记得刚接手一个项目时,看到前辈用同步请求做了个图片加载库。结果用户翻相册时界面直接卡死——主线程被堵死了。说白了,选错请求方式,用户体验立马崩给你看。
今天我把这两种方式的区别、适用场景、回调陷阱一次性讲清楚。以后你写网络请求,心里就有谱了。
同步请求:简单但危险
同步请求调用 execute(),当前线程会被阻塞,直到拿到响应或抛出异常。
// 同步请求(千万别在主线程跑)
Request request = new Request.Builder()
.url("https://api.example.com/data")
.build();
try {
Response response = client.newCall(request).execute();
// 拿到响应,继续处理
String body = response.body().string();
// ...
} catch (IOException e) {
e.printStackTrace();
}
⚠️ 不要在主线程调用 execute()——Android 会直接抛 NetworkOnMainThreadException。但就算不抛,卡 UI 也是找死。
同步请求适合哪些场景?我个人习惯用在 小规模、线性的后台任务 里。比如批量上传前的 刷新、测试环境下的简单调试。不需要同时处理多个请求,代码逻辑从上往下读就行,简单直接。
💡 工具方法里如果用了同步请求,记得加个线程参数或者内部切换到子线程。我一个同事就忘了这茬,上线后 ANR 报警炸了。
异步请求:优雅且灵活
异步请求调用 enqueue(Callback callback),OkHttp 内部会开子线程去执行网络请求,请求结束后回调你的 callback。
// 异步请求(推荐日常使用)
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
// 网络异常、超时等,运行在 OkHttp 的工作线程
// 记得切回主线程更新 UI
}
@Override
public void onResponse(Call call, Response response) throws IOException {
// 拿到响应,依然在子线程
// 解析完 body 后切回主线程
}
});
⚠️ 重点注意:onResponse 和 onFailure 运行在 OkHttp 的 工作线程,不能直接更新 UI。必须手动 runOnUiThread 或 Handler 切回去。
异步请求的优势很明显:非阻塞,主线程流畅,可以同时发起多个请求。我做过一个搜索建议功能,用户每输入一个字就发一个异步请求,前端用 debounce 控制,后端并发处理。要是用同步,一个请求就把 UI 堵死了。
同步 vs 异步:一张表搞清楚
| 对比维度 | 同步 (execute) | 异步 (enqueue) |
|---|---|---|
| 阻塞调用线程 | 是,直到响应返回 | 否,立即返回 |
| 执行线程 | 当前调用线程 | OkHttp 内部线程池 |
| 回调方式 | 直接返回 Response | Callback 接口回调 |
| 适用场景 | 后台任务、测试、批量顺序 | 大多数日常请求、并发、UI 交互 |
| 线程安全问题 | 需要外部保证 | 回调内注意切主线程 |
回调处理的三个坑
光知道怎么调用还不够。回调里藏着三个常见问题,我当年全踩过。
1. 回调线程错乱
回调无论是 onResponse 还是 onFailure,都在 OkHttp 的工作线程。你想更新 UI 就必须切线程。很多人忘记这一步,直接写 textView.setText(...),运行时直接崩溃。我的习惯是:在回调第一行就判断是否需要切主线程。
@Override
public void onResponse(Call call, Response response) {
// 先取数据,别在这切
final String data = response.body().string();
// 最后统一切主线程
runOnUiThread(() -> textView.setText(data));
}
2. 请求泄露与 cancel
Activity 销毁了,异步请求还在跑,回调里却拿着过期的 View 引用——典型的 生命周期泄漏。我在项目里见过一个老版本,用户退出登录后,列表的图片请求还在回调更新,直接空指针。
解决办法:在 onDestroy 或 onStop 里调用 call.cancel()。对于多个请求,可以用 OkHttpClient.dispatcher().cancelAll() 批量取消。
⚠️ Cancel 之后,回调仍然可能被调用?不会。OkHttp 设计得不错:一旦 cancel,已取消的请求不会再触发回调。但你得保证 cancel 时机正确——别在 enqueue 后立刻 cancel,那等于白做。
3. Callback 内部的内存泄漏
异步回调里常常持有外部类的隐式引用,比如匿名内部类或 lambda。如果你忘记清理,整个 Activity 都无法被 GC。我建议的做法:使用弱引用或者手动置空。当然,配合 lifecycleScope 在 Coroutine 里处理会更安全——后面章节我会专门讲。
流程对比图:一眼看透同步与异步
下面这张图把两种请求的生命周期画出来了,方便你对照理解。
💡 图中同步流程的“阻塞”阶段,如果网络慢,主线程卡死至少十几秒;异步流程的“立即返回”让主线程瞬间解放。这就是为什么日常开发我 99% 用异步。
小结
好了,同步和异步的核心区别很简单:同步堵你,异步不堵。但实际项目中,异步的回调管理、取消机制、内存泄漏才是真正的坑。我当年在 IM 项目里就是因为忘了 cancel,导致多个请求泄露,最后 OOM。从那以后,我每个 enqueue 后面都跟着一个生命周期绑定。