11.01 网络编程-OkHttp

2025/3/28 Android 开发基础

11.00 讲清了 HTTP 本身,本节进入实现:OkHttp 是 Android 网络库的事实标准(Retrofit、Glide 的网络层、绝大多数 App 的网络栈都基于它)。本节覆盖基本用法与它最重要的架构设计——拦截器责任链。

# 一、基本使用

# 1.1 依赖

implementation 'com.squareup.okhttp3:okhttp:4.10.0'   // Kotlin实现,Java/Kotlin均可调用

# 1.2 同步与异步请求

public class OkHttpDemo {
    // OkHttpClient必须是单例:它管理连接池和线程池,重复创建会浪费资源
    private static final OkHttpClient CLIENT = new OkHttpClient();

    // 同步请求:execute()阻塞当前线程,禁止在主线程调用(见10.03红线)
    public static void syncGet() throws IOException {
        Request request = new Request.Builder()
                .url("https://api.example.com/users/1")
                .build();
        try (Response response = CLIENT.newCall(request).execute()) {
            if (response.isSuccessful() && response.body() != null) {
                Log.i(TAG, "同步结果:" + response.body().string());
            }
        }
    }

    // 异步请求:enqueue()入队OkHttp自己的线程池执行,回调切回调用线程
    public static void asyncGet() {
        Request request = new Request.Builder()
                .url("https://api.example.com/users/1")
                .build();

        CLIENT.newCall(request).enqueue(new Callback() {
            // 请求失败(网络异常,不含4xx/5xx业务错误——那也是成功的response)
            @Override
            public void onFailure(Call call, IOException e) {
                Log.e(TAG, "请求失败", e);
            }

            // 拿到响应(注意:回调运行在OkHttp的子线程,更新UI必须切主线程)
            @Override
            public void onResponse(Call call, Response response) throws IOException {
                String body = response.body() != null ? response.body().string() : "";
                // 两种切回主线程方式:
                // 1) Handler(10.01)
                new Handler(Looper.getMainLooper()).post(() -> updateUi(body));
                // 2) 直接用主线程Handler写法,或配合Retrofit(11.02)自动切线程
            }
        });
    }
}

易错点:response.body().string() 只能调用一次(流式读取),读后 body 为空——需要多次使用先存成字符串。

# 1.3 POST 请求:表单 / JSON / 文件

// 1. 表单提交(application/x-www-form-urlencoded)
Request formRequest = new Request.Builder()
        .url("https://api.example.com/login")
        .post(new FormBody.Builder()
                .add("username", "JiangMing")
                .add("password", "******")
                .build())
        .build();

// 2. JSON提交(application/json)——最常见
MediaType JSON = MediaType.parse("application/json; charset=utf-8");
String json = "{\"name\":\"JiangMing\",\"age\":26}";
Request jsonRequest = new Request.Builder()
        .url("https://api.example.com/users")
        .post(RequestBody.create(json, JSON))
        .build();

// 3. 文件上传(multipart/form-data)
File image = new File(getCacheDir(), "avatar.png");
RequestBody fileBody = RequestBody.create(image, MediaType.parse("image/png"));
Request multipartRequest = new Request.Builder()
        .url("https://api.example.com/upload")
        .post(new MultipartBody.Builder()
                .setType(MultipartBody.FORM)
                .addFormDataPart("desc", "头像")
                .addFormDataPart("file", image.getName(), fileBody)
                .build())
        .build();

# 1.4 客户端配置:超时与取消

OkHttpClient client = new OkHttpClient.Builder()
        .connectTimeout(10, TimeUnit.SECONDS)   // TCP连接超时(对应11.00的握手阶段)
        .readTimeout(15, TimeUnit.SECONDS)      // 读响应超时
        .writeTimeout(15, TimeUnit.SECONDS)     // 写请求超时
        .retryOnConnectionFailure(true)         // 连接失败自动重试
        .build();

// 取消请求(页面销毁时调用,避免泄漏与无效回调)
Call call = CLIENT.newCall(request);
call.enqueue(callback);
call.cancel();

# 二、拦截器责任链(核心架构)

OkHttp 的所有能力——重试、Cookie、Gzip、缓存、连接复用——都建立在拦截器责任链上,这也是自定义网络层功能的扩展点:

OkHttp拦截器责任链示意图

# 2.1 工作机制

每个拦截器实现 intercept(Chain):加工请求 → chain.proceed(request) 交给下一层 → 拿到响应再加工 → 返回。请求逐层下行、响应逐层上行( onion 模型),任何一层都可以短路(直接返回缓存)或抛异常终止。

内置拦截器(自上而下):RetryAndFollowUp(重试/重定向)→ Bridge(补齐 Host/Content-Length,处理 Cookie、透明 Gzip)→ Cache(命中缓存直接短路返回)→ Connect(从连接池取复用连接)→ 网络拦截器 → CallServer(真正的 socket 读写)。

# 2.2 自定义拦截器:统一加 Token

最常见的应用场景——所有请求自动携带登录态:

public class TokenInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request original = chain.request();
        Request request = original.newBuilder()
                .header("Authorization", "Bearer " + TokenManager.getToken())
                .header("X-App-Version", BuildConfig.VERSION_NAME)  // 公共参数统一注入
                .method(original.method(), original.body())
                .build();
        return chain.proceed(request);   // 交给下一层
    }
}

// 注册:应用拦截器(整条链只走一次,推荐业务功能用它)
OkHttpClient client = new OkHttpClient.Builder()
        .addInterceptor(new TokenInterceptor())
        .addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
        .build();

应用拦截器 vs 网络拦截器:

对比 addInterceptor addNetworkInterceptor
位置 链最前端 紧贴真实 IO(重定向后仍会走)
调用次数 一次逻辑请求一次 每次真实网络连接都走
典型用途 业务功能(token、公共参数、日志) 观测真实流量(抓包调试、网络指标)

日志拦截器是现成的:implementation 'com.squareup.okhttp3:logging-interceptor:x.x.x',debug 构建才加,BODY 级别会打印完整报文(release 打印会泄漏敏感信息且耗性能)。

# 三、连接池与缓存

# 3.1 连接池复用

11.00 的生命周期四步里,DNS/TCP/TLS 握手最贵。OkHttp 通过连接池(默认 5 个空闲连接、保活 5 分钟)+ HTTP keep-alive 复用 TCP 连接:同一 host 的后续请求跳过握手直达 CallServer。这就是"单例 OkHttpClient"最重要的原因——池在 client 里,新建 client = 丢掉全部复用能力。

# 3.2 HTTP 缓存

// 缓存目录给足空间(默认50MB上限可调),命中304时直接用本地副本
OkHttpClient client = new OkHttpClient.Builder()
        .cache(new Cache(new File(getCacheDir(), "http_cache"), 50L * 1024 * 1024))
        .build();

缓存行为由服务端的响应头(Cache-Control、ETag)驱动:CacheInterceptor 检查本地副本 → 未过期直接返回(请求都不发)→ 过期则带 If-None-Match 询问,服务器回 304 就复用本地。弱网优化与缓存策略的完整讨论见 11.03。

# 四、常见问题与陷阱

  1. 回调在子线程:enqueue 的 onResponse/onFailure 不在主线程,更新 UI 必须切线程(10.01 范式);Retrofit 的 suspend/回调封装能省掉这一步。
  2. 4xx/5xx 不走 onFailure:onFailure 只对应 IO 异常(超时、断网);HTTP 错误状态码也是 onResponse,需自行判断 isSuccessful / 状态码。
  3. body 只能读一次:string()/bytes() 消费流;拦截器里读 body 后要重新构造(peekBody 或 response.newBuilder().body())。
  4. 同步请求跑主线程:execute() 直接 NetworkOnMainThreadException。
  5. 每次请求 new OkHttpClient:连接池/线程池失效,性能差且易泄漏,务必单例。
  6. 上传/下载进度:拦截器包装 RequestBody/ResponseBody,在 writeTo/readFrom 中按字节数回调进度(大文件场景标配)。

# 结束

本节介绍了 OkHttp:同步/异步两种请求姿势、GET/表单/JSON/Multipart 四种请求体、超时配置与取消,核心是拦截器责任链架构(内置五层 + 应用/网络两类拦截器的分工)、连接池复用与 HTTP 缓存三块设计,以及单例 client、回调线程、body 一次性读取等高频陷阱。OkHttp 解决"怎么发请求",下一节的 Retrofit 解决"请求怎么写得优雅":11.02 Retrofit。更多内容请参考:OkHttp 官方文档 (opens new window)。