11.01 网络编程-OkHttp
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、缓存、连接复用——都建立在拦截器责任链上,这也是自定义网络层功能的扩展点:
# 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。
# 四、常见问题与陷阱
- 回调在子线程:
enqueue的onResponse/onFailure不在主线程,更新 UI 必须切线程(10.01 范式);Retrofit 的 suspend/回调封装能省掉这一步。 - 4xx/5xx 不走 onFailure:
onFailure只对应 IO 异常(超时、断网);HTTP 错误状态码也是onResponse,需自行判断isSuccessful/ 状态码。 - body 只能读一次:
string()/bytes()消费流;拦截器里读 body 后要重新构造(peekBody或response.newBuilder().body())。 - 同步请求跑主线程:
execute()直接NetworkOnMainThreadException。 - 每次请求 new OkHttpClient:连接池/线程池失效,性能差且易泄漏,务必单例。
- 上传/下载进度:拦截器包装 RequestBody/ResponseBody,在
writeTo/readFrom中按字节数回调进度(大文件场景标配)。
# 结束
本节介绍了 OkHttp:同步/异步两种请求姿势、GET/表单/JSON/Multipart 四种请求体、超时配置与取消,核心是拦截器责任链架构(内置五层 + 应用/网络两类拦截器的分工)、连接池复用与 HTTP 缓存三块设计,以及单例 client、回调线程、body 一次性读取等高频陷阱。OkHttp 解决"怎么发请求",下一节的 Retrofit 解决"请求怎么写得优雅":11.02 Retrofit。更多内容请参考:OkHttp 官方文档 (opens new window)。