同步与异步请求:你的第一个选择

用了 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 里处理会更安全——后面章节我会专门讲。

流程对比图:一眼看透同步与异步

下面这张图把两种请求的生命周期画出来了,方便你对照理解。

同步 vs 异步 请求流程 同步请求 主线程调用 execute() 阻塞,等待网络响应 返回 Response 或抛异常 异步请求 主线程调用 enqueue() 立即返回,主线程继续 子线程执行网络请求 回调 onResponse / onFailure ⚠️ 异步回调运行在子线程,更新 UI 必须切回主线程 Activity 销毁时务必 cancel() 避免泄漏

💡 图中同步流程的“阻塞”阶段,如果网络慢,主线程卡死至少十几秒;异步流程的“立即返回”让主线程瞬间解放。这就是为什么日常开发我 99% 用异步。

小结

好了,同步和异步的核心区别很简单:同步堵你,异步不堵。但实际项目中,异步的回调管理、取消机制、内存泄漏才是真正的坑。我当年在 IM 项目里就是因为忘了 cancel,导致多个请求泄露,最后 OOM。从那以后,我每个 enqueue 后面都跟着一个生命周期绑定。