第一部分:OkHttp 基础
第一章:OkHttp 概述(7 题)
1.1 OkHttp是什么?它的作用是什么?
答案:
OkHttp是一个开源的HTTP客户端库,由Square公司开发,用于Android和Java应用程序中发送HTTP请求和处理HTTP响应。
OkHttp的定义:
- 一个HTTP/HTTP2客户端
- 支持同步和异步请求
- 提供连接池、缓存、拦截器等高级特性
- 默认支持GZIP压缩、响应缓存等
OkHttp的主要作用:
-
发送HTTP请求
- 支持GET、POST、PUT、DELETE等HTTP方法
- 支持HTTP/1.1和HTTP/2.0协议
-
处理HTTP响应
- 自动处理响应头
- 支持流式响应处理
- 自动解压GZIP响应
-
网络优化
- 连接池复用,减少连接开销
- 自动重试失败的请求
- 支持请求和响应缓存
-
安全性
- 支持HTTPS
- 支持证书锁定(Certificate Pinning)
- 支持TLS/SSL配置
核心特点:
- 高效:连接池复用,减少延迟
- 可靠:自动重试和恢复
- 易用:简洁的API设计
- 灵活:拦截器机制,可扩展性强
1.2 OkHttp的特点和优势有哪些?
答案:
OkHttp具有以下特点和优势:
1. 性能优势
-
连接池复用
- 复用TCP连接,减少握手开销
- 支持HTTP/2.0多路复用
- 显著提升网络性能
-
自动压缩
- 自动处理GZIP压缩
- 减少数据传输量
- 提升传输效率
-
响应缓存
- 支持HTTP缓存机制
- 减少重复请求
- 离线访问能力
2. 功能优势
-
拦截器机制
- 应用拦截器和网络拦截器
- 可自定义请求和响应处理
- 支持日志、认证、重试等功能
-
异步支持
- 支持同步和异步请求
- 基于回调的异步处理
- 避免阻塞主线程
-
错误处理
- 自动重试机制
- 详细的错误信息
- 网络异常处理
3. 易用性优势
-
简洁的API
OkHttpClient client = new OkHttpClient(); Request request = new Request.Builder() .url("https://api.example.com/data") .build(); Response response = client.newCall(request).execute(); -
链式调用
- Builder模式设计
- 代码可读性强
- 配置灵活
4. 可靠性优势
-
自动重试
- 网络失败自动重试
- 可配置重试策略
- 提高请求成功率
-
超时控制
- 连接超时、读取超时、写入超时
- 防止长时间等待
- 提升用户体验
5. 安全性优势
- HTTPS支持
- 完整的TLS/SSL支持
- 证书验证
- 证书锁定功能
6. 兼容性优势
- 广泛使用
- Retrofit、Picasso等框架的底层实现
- 社区活跃,文档完善
- 持续更新维护
与其他框架对比:
| 特性 | OkHttp | HttpURLConnection |
|---|---|---|
| 连接池 | ✓ | ✗ |
| HTTP/2.0 | ✓ | ✗ |
| 拦截器 | ✓ | ✗ |
| 缓存 | ✓ | 手动 |
| 易用性 | 高 | 低 |
1.3 OkHttp和HttpURLConnection的区别是什么?
答案:
OkHttp和HttpURLConnection是Android中两种主要的HTTP客户端,它们的主要区别如下:
| 对比项 | OkHttp | HttpURLConnection |
|---|---|---|
| 架构设计 | 基于拦截器链,模块化架构,易于扩展 | Java标准库,简单请求-响应模型,功能固定 |
| 连接管理 | 连接池复用,自动管理连接生命周期 | 无连接池,每次可能创建新连接,需手动管理 |
| HTTP协议 | 支持HTTP/1.1和HTTP/2.0 | 仅支持HTTP/1.1 |
| 性能 | 连接复用,性能提升30-50% | 频繁创建连接,性能较低 |
| 拦截器 | ✓ 支持拦截器机制 | ✗ 不支持 |
| 自动重试 | ✓ 支持自动重试 | ✗ 需手动实现 |
| 响应缓存 | ✓ 支持HTTP缓存 | ✗ 需手动实现 |
| GZIP压缩 | ✓ 自动处理GZIP | ✗ 需手动处理 |
| Cookie管理 | ✓ 支持CookieJar | ✗ 需手动管理 |
| 日志功能 | ✓ 支持请求/响应日志 | ✗ 无内置日志 |
| 错误处理 | 自动重试,详细异常信息,异常分类 | 需手动实现重试,异常信息简单 |
| API易用性 | 简洁的Builder模式API | 代码较繁琐,需要多步设置 |
| 依赖 | 需要添加外部依赖库 | Java标准库,无需额外依赖 |
| 包体积 | 增加约200KB | 无额外体积 |
| 适用场景 | 现代Android应用、高性能需求、与Retrofit集成 | 简单请求、无依赖要求、包体积敏感 |
代码示例对比:
OkHttp:
// 简洁的API
Request request = new Request.Builder()
.url("https://api.example.com/data")
.post(RequestBody.create(MediaType.parse("application/json"), json))
.build();
Response response = client.newCall(request).execute();
HttpURLConnection:
// 代码较繁琐
URL url = new URL("https://api.example.com/data");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setDoOutput(true);
conn.setRequestProperty("Content-Type", "application/json");
// ... 更多设置
总结:
- OkHttp:功能强大、性能优秀、易用性高,是现代Android开发的首选
- HttpURLConnection:简单直接、无额外依赖,适合简单场景或对包体积有严格要求的场景
1.4 OkHttp的版本历史和发展历程是什么?
答案:
OkHttp由Square公司开发,是Android和Java平台最流行的HTTP客户端库之一。
版本历史:
1. 早期版本(2012-2014)
- OkHttp 1.x
- 2012年首次发布
- 提供基本的HTTP客户端功能
- 支持同步和异步请求
- 引入连接池概念
2. 重要更新(2014-2016)
- OkHttp 2.x
- 2014年发布
- 引入拦截器(Interceptor)机制
- 支持HTTP/2.0协议
- 改进连接池实现
- 更好的错误处理
3. 现代化版本(2016-至今)
-
OkHttp 3.x
- 2016年发布
- 重大API改进:更简洁的API设计
- 性能优化:更好的连接复用
- 安全性增强:TLS/SSL改进
- 拦截器增强:应用拦截器和网络拦截器
-
OkHttp 4.x
- 2019年发布
- Kotlin支持:更好的Kotlin互操作性
- 协程支持:支持Kotlin Coroutines
- API改进:更现代的API设计
- 性能提升:进一步优化
-
OkHttp 5.x(当前版本)
- 持续更新中
- HTTP/3支持:支持QUIC协议
- 性能优化:持续的性能改进
- 安全性:最新的TLS支持
主要里程碑:
- 2012年:OkHttp首次发布
- 2014年:引入拦截器机制
- 2016年:OkHttp 3.x发布,API重大改进
- 2019年:OkHttp 4.x发布,Kotlin支持
- 2020年至今:持续更新,支持HTTP/3
发展特点:
- 持续更新:Square公司持续维护
- 社区活跃:GitHub上star数超过45k
- 广泛采用:被Retrofit、Picasso等框架使用
- 性能优化:每个版本都有性能提升
- 安全性:紧跟最新的安全标准
当前状态:
- 最新版本:OkHttp 5.x
- 维护状态:积极维护中
- 使用情况:Android开发的主流选择
1.5 如何添加OkHttp依赖?
答案:
添加OkHttp依赖有多种方式,取决于项目使用的构建工具:
1. Gradle(Android项目)
在build.gradle(Module级别)中添加:
dependencies {
// OkHttp核心库
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
// 如果需要日志拦截器(可选)
implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0'
}
2. 依赖说明
核心依赖:
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
- 包含OkHttp的所有核心功能
- 支持HTTP/1.1和HTTP/2.0
- 包含连接池、缓存等功能
可选依赖:
-
日志拦截器
implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0'- 用于调试和日志记录
- 打印请求和响应信息
-
MockWebServer(测试用)
testImplementation 'com.squareup.okhttp3:mockwebserver:4.12.0'- 用于单元测试
- 模拟HTTP服务器
3. 依赖冲突处理
如果遇到依赖冲突,可以使用:
configurations.all {
resolutionStrategy {
force 'com.squareup.okhttp3:okhttp:4.12.0'
}
}
1.6 OkHttp支持哪些HTTP协议版本?
答案:
OkHttp支持多种HTTP协议版本,具体如下:
1. HTTP/1.0
- 支持情况:✓ 完全支持
- 特点:每个请求一个连接
- 使用场景:兼容旧服务器
2. HTTP/1.1
- 支持情况:✓ 完全支持(默认)
- 特点:
- 持久连接(Keep-Alive)
- 管道化(Pipelining)
- 分块传输编码
- 使用场景:当前最常用的版本
3. HTTP/2.0
- 支持情况:✓ 完全支持
- 特点:
- 多路复用:单连接多请求
- 头部压缩:HPACK压缩
- 服务器推送:服务器主动推送资源
- 二进制分帧:更高效的传输
- 使用场景:现代服务器,性能要求高
4. HTTP/3(QUIC)
- 支持情况:✓ OkHttp 5.x支持
- 特点:
- 基于UDP的QUIC协议
- 更快的连接建立
- 更好的移动网络支持
- 使用场景:最新协议,性能最优
协议选择机制:
OkHttp会自动选择可用的最高版本:
1. 尝试HTTP/2.0(如果服务器支持)
2. 降级到HTTP/1.1
3. 降级到HTTP/1.0(如果需要)
配置HTTP/2.0:
OkHttpClient client = new OkHttpClient.Builder()
.protocols(Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1))
.build();
协议对比:
| 特性 | HTTP/1.1 | HTTP/2.0 | HTTP/3 |
|---|---|---|---|
| 多路复用 | ✗ | ✓ | ✓ |
| 头部压缩 | ✗ | ✓ | ✓ |
| 连接建立 | TCP握手 | TCP握手 | QUIC(更快) |
| 传输层 | TCP | TCP | UDP |
| 移动网络 | 一般 | 好 | 优秀 |
实际使用:
- HTTP/1.1:默认,兼容性最好
- HTTP/2.0:性能更好,需要服务器支持
- HTTP/3:最新,性能最优,需要OkHttp 5.x
1.7 OkHttp的架构设计是什么?
答案:
OkHttp采用分层架构和拦截器链设计,具有高度的模块化和可扩展性。
1. 整体架构
┌─────────────────────────────────────┐
│ Application Layer │
│ (OkHttpClient, Request, Call) │
└─────────────────────────────────────┘
│
┌─────────────────────────────────────┐
│ Interceptor Chain Layer │
│ (Application & Network Interceptors)│
└─────────────────────────────────────┘
│
┌─────────────────────────────────────┐
│ Connection Layer │
│ (ConnectionPool, Route, RealConnection)│
└─────────────────────────────────────┘
│
┌─────────────────────────────────────┐
│ Network Layer │
│ (Socket, TLS, HTTP/2) │
└─────────────────────────────────────┘
2. 核心组件
(1)OkHttpClient
- 作用:客户端配置和单例
- 职责:
- 管理连接池
- 配置拦截器
- 设置超时时间
- 管理Dispatcher
(2)Request
- 作用:封装HTTP请求
- 包含:
- URL
- 请求方法(GET、POST等)
- 请求头
- 请求体
(3)Response
- 作用:封装HTTP响应
- 包含:
- 状态码
- 响应头
- 响应体
- 响应来源(缓存/网络)
(4)Call
- 作用:执行请求的接口
- 实现:
RealCall:实际执行请求- 支持同步和异步
(5)Interceptor(拦截器)
- 作用:处理请求和响应
- 类型:
- Application Interceptor:应用拦截器
- Network Interceptor:网络拦截器
(6)ConnectionPool(连接池)
- 作用:管理TCP连接
- 功能:
- 连接复用
- 连接清理
- 连接数量控制
(7)Dispatcher(调度器)
- 作用:管理异步请求
- 功能:
- 请求队列管理
- 线程池管理
- 请求执行控制
3. 请求执行流程
1. 创建Request
↓
2. 创建Call(RealCall)
↓
3. 执行拦截器链
├── Application Interceptors
├── RetryAndFollowUpInterceptor
├── BridgeInterceptor
├── CacheInterceptor
├── ConnectInterceptor
├── Network Interceptors
└── CallServerInterceptor
↓
4. 获取连接(ConnectionPool)
↓
5. 发送请求(Socket)
↓
6. 接收响应
↓
7. 处理响应(拦截器链返回)
↓
8. 返回Response
4. 拦截器链设计
拦截器执行顺序:
请求方向:
Application Interceptors
→ RetryAndFollowUpInterceptor
→ BridgeInterceptor
→ CacheInterceptor
→ ConnectInterceptor
→ Network Interceptors
→ CallServerInterceptor
响应方向(反向):
CallServerInterceptor
→ Network Interceptors
→ ConnectInterceptor
→ CacheInterceptor
→ BridgeInterceptor
→ RetryAndFollowUpInterceptor
→ Application Interceptors
5. 设计模式
(1)责任链模式
- 拦截器链的实现
- 每个拦截器处理特定职责
(2)建造者模式
- Request.Builder
- OkHttpClient.Builder
- 链式调用,配置灵活
(3)单例模式
- OkHttpClient通常作为单例
- 共享连接池和配置
(4)策略模式
- 不同的拦截器策略
- 不同的缓存策略
6. 架构优势
(1)模块化
- 各组件职责清晰
- 易于理解和维护
(2)可扩展性
- 拦截器机制,易于扩展
- 插件化设计
(3)高性能
- 连接池复用
- 异步处理
- 缓存机制
(4)灵活性
- 可配置性强
- 支持多种使用场景
7. 关键设计决策
(1)拦截器链
- 统一处理请求和响应
- 功能解耦,易于扩展
(2)连接池
- 复用TCP连接
- 减少连接开销
(3)异步设计
- Dispatcher管理异步请求
- 避免阻塞主线程
(4)缓存机制
- HTTP缓存标准
- 减少网络请求
总结: OkHttp的架构设计体现了高内聚、低耦合的原则,通过拦截器链实现了功能的模块化和可扩展性,通过连接池和缓存机制实现了高性能。
第二章:OkHttp 基本使用(6 题)
2.1 如何使用OkHttp发送HTTP请求(GET、POST、PUT、DELETE)?
答案:
OkHttp支持所有HTTP方法,基本使用方式相同,主要区别在于请求体的设置。
1. 基本步骤
// 1. 创建OkHttpClient(建议使用单例)
OkHttpClient client = new OkHttpClient();
// 2. 创建Request并执行
try (Response response = client.newCall(request).execute()) {
if (response.isSuccessful()) {
String body = response.body().string();
Log.d("OkHttp", "成功:" + body);
}
}
2. GET请求(获取资源)
// GET请求不需要请求体
Request request = new Request.Builder()
.url("https://api.example.com/users")
.get() // 可省略,默认就是GET
.build();
try (Response response = client.newCall(request).execute()) {
if (response.isSuccessful()) {
String body = response.body().string();
}
}
3. POST请求(创建资源)
// 发送JSON数据
String json = "{\"name\":\"张三\",\"age\":25}";
RequestBody requestBody = RequestBody.create(
json,
MediaType.parse("application/json; charset=utf-8")
);
Request request = new Request.Builder()
.url("https://api.example.com/users")
.post(requestBody)
.build();
4. PUT请求(更新资源)
// PUT请求与POST类似,只是方法不同
String json = "{\"name\":\"李四\",\"age\":30}";
RequestBody requestBody = RequestBody.create(
json,
MediaType.parse("application/json; charset=utf-8")
);
Request request = new Request.Builder()
.url("https://api.example.com/users/123")
.put(requestBody)
.build();
5. DELETE请求(删除资源)
// DELETE请求通常不需要请求体
Request request = new Request.Builder()
.url("https://api.example.com/users/123")
.delete()
.build();
6. HTTP方法对比
| 方法 | 用途 | 请求体 | 幂等性 | 示例 |
|---|---|---|---|---|
| GET | 获取资源 | 无 | 是 | 查询用户列表 |
| POST | 创建资源 | 有 | 否 | 创建新用户 |
| PUT | 更新资源 | 有 | 是 | 更新用户信息 |
| DELETE | 删除资源 | 无 | 是 | 删除用户 |
7. 常用请求体类型
(1)JSON数据
RequestBody requestBody = RequestBody.create(
json,
MediaType.parse("application/json; charset=utf-8")
);
(2)表单数据
RequestBody formBody = new FormBody.Builder()
.add("username", "zhangsan")
.add("password", "123456")
.build();
(3)多部分表单(文件上传)
RequestBody requestBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("file", "file.jpg",
RequestBody.create(file, MediaType.parse("image/jpeg")))
.build();
注意事项:
- GET和DELETE通常不需要请求体
- POST和PUT需要设置请求体和Content-Type
- 使用try-with-resources自动关闭Response
- 网络请求应在后台线程执行
2.2 如何设置请求头(Headers)?
答案:
OkHttp提供了多种方式设置请求头:
1. 使用header()方法(覆盖)
Request request = new Request.Builder()
.url("https://api.example.com/data")
.header("Authorization", "Bearer token123")
.header("Content-Type", "application/json")
.header("User-Agent", "MyApp/1.0")
.build();
特点:
- 如果已存在同名header,会被覆盖
- 每个header只能有一个值
2. 使用addHeader()方法(添加)
Request request = new Request.Builder()
.url("https://api.example.com/data")
.addHeader("Accept", "application/json")
.addHeader("Accept", "application/xml") // 可以添加多个同名header
.build();
特点:
- 可以添加多个同名header
- 不会覆盖已存在的header
3. 使用headers()方法(批量设置)
Headers headers = new Headers.Builder()
.add("Authorization", "Bearer token123")
.add("Content-Type", "application/json")
.add("Accept", "application/json")
.build();
Request request = new Request.Builder()
.url("https://api.example.com/data")
.headers(headers)
.build();
4. 常用请求头示例
Request request = new Request.Builder()
.url("https://api.example.com/data")
// 认证
.header("Authorization", "Bearer " + token)
// 内容类型
.header("Content-Type", "application/json")
// 接受类型
.header("Accept", "application/json")
// 用户代理
.header("User-Agent", "MyApp/1.0 (Android)")
// 语言
.header("Accept-Language", "zh-CN,zh;q=0.9")
// 编码
.header("Accept-Encoding", "gzip, deflate")
// 自定义header
.header("X-API-Key", "your-api-key")
.header("X-Request-ID", UUID.randomUUID().toString())
.build();
5. 动态设置请求头
public Request buildRequest(String url, String token) {
Request.Builder builder = new Request.Builder()
.url(url);
if (token != null) {
builder.header("Authorization", "Bearer " + token);
}
builder.header("Content-Type", "application/json");
return builder.build();
}
6. 使用拦截器统一添加请求头
// 在拦截器中添加公共请求头
public class HeaderInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request original = chain.request();
Request request = original.newBuilder()
.header("Authorization", "Bearer " + getToken())
.header("User-Agent", "MyApp/1.0")
.build();
return chain.proceed(request);
}
}
// 使用
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new HeaderInterceptor())
.build();
7. 注意事项
- header():覆盖同名header
- addHeader():添加header,可重复
- headers():批量设置,会清除之前的headers
- 敏感信息:不要在URL中传递,使用header传递
- 大小写:HTTP header名称不区分大小写,但建议使用标准格式
2.3 如何设置请求参数(Query Parameters)?
答案:
OkHttp提供了多种方式设置查询参数:
1. 直接在URL中添加
String url = "https://api.example.com/users?page=1&limit=10&sort=name";
Request request = new Request.Builder()
.url(url)
.build();
2. 使用HttpUrl.Builder(推荐)
HttpUrl httpUrl = HttpUrl.parse("https://api.example.com/users")
.newBuilder()
.addQueryParameter("page", "1")
.addQueryParameter("limit", "10")
.addQueryParameter("sort", "name")
.build();
Request request = new Request.Builder()
.url(httpUrl)
.build();
3. 动态构建查询参数
public Request buildRequestWithParams(String baseUrl, Map<String, String> params) {
HttpUrl.Builder urlBuilder = HttpUrl.parse(baseUrl).newBuilder();
for (Map.Entry<String, String> entry : params.entrySet()) {
urlBuilder.addQueryParameter(entry.getKey(), entry.getValue());
}
Request request = new Request.Builder()
.url(urlBuilder.build())
.build();
return request;
}
// 使用
Map<String, String> params = new HashMap<>();
params.put("page", "1");
params.put("limit", "10");
Request request = buildRequestWithParams("https://api.example.com/users", params);
4. 处理特殊字符
// HttpUrl会自动编码特殊字符
HttpUrl httpUrl = HttpUrl.parse("https://api.example.com/search")
.newBuilder()
.addQueryParameter("q", "hello world") // 自动编码为 "hello%20world"
.addQueryParameter("category", "电子产品")
.build();
5. 数组参数
// 某些API支持数组参数
HttpUrl httpUrl = HttpUrl.parse("https://api.example.com/products")
.newBuilder()
.addQueryParameter("ids", "1")
.addQueryParameter("ids", "2")
.addQueryParameter("ids", "3")
.build();
// 结果:?ids=1&ids=2&ids=3
7. 注意事项
- URL编码:HttpUrl会自动处理URL编码
- 空值处理:空字符串和null会被编码为不同的值
- 特殊字符:使用HttpUrl.Builder可以避免手动编码错误
- 参数顺序:查询参数的顺序通常不影响结果
2.4 如何发送JSON数据?
答案:
发送JSON数据需要创建JSON格式的RequestBody。
1. 基本方法
// 1. 准备JSON字符串
String json = "{\"name\":\"张三\",\"age\":25,\"email\":\"zhangsan@example.com\"}";
// 2. 创建RequestBody
RequestBody requestBody = RequestBody.create(
json,
MediaType.parse("application/json; charset=utf-8")
);
// 3. 创建POST请求(Content-Type会自动从RequestBody中获取)
Request request = new Request.Builder()
.url("https://api.example.com/users")
.post(requestBody)
.build();
// 4. 执行请求
try (Response response = client.newCall(request).execute()) {
if (response.isSuccessful()) {
String result = response.body().string();
Log.d("OkHttp", "响应:" + result);
}
}
2. 使用Gson(推荐)
// 添加依赖:implementation 'com.google.code.gson:gson:2.10.1'
// 定义数据类
public class User {
private String name;
private int age;
private String email;
public User(String name, int age, String email) {
this.name = name;
this.age = age;
this.email = email;
}
}
// 使用Gson转换
Gson gson = new Gson();
User user = new User("张三", 25, "zhangsan@example.com");
String json = gson.toJson(user);
RequestBody requestBody = RequestBody.create(
json,
MediaType.parse("application/json; charset=utf-8")
);
Request request = new Request.Builder()
.url("https://api.example.com/users")
.post(requestBody)
.build();
3. 解析JSON响应
try (Response response = client.newCall(request).execute()) {
if (response.isSuccessful()) {
String json = response.body().string();
Gson gson = new Gson();
User user = gson.fromJson(json, User.class);
}
}
4. 注意事项
- Content-Type:必须设置为
application/json - 字符编码:使用UTF-8编码
- JSON格式:确保JSON格式正确
- 空值处理:注意null值的处理
- 日期格式:日期需要转换为字符串或使用特定格式
2.5 如何发送表单数据(Form Data)?
答案:
发送表单数据使用FormBody类,适用于application/x-www-form-urlencoded格式。
1. 基本用法
// 创建FormBody
RequestBody formBody = new FormBody.Builder()
.add("username", "zhangsan")
.add("password", "123456")
.add("email", "zhangsan@example.com")
.build();
// 创建POST请求
Request request = new Request.Builder()
.url("https://api.example.com/login")
.post(formBody)
.build();
// 执行请求
try (Response response = client.newCall(request).execute()) {
if (response.isSuccessful()) {
String result = response.body().string();
Log.d("OkHttp", "登录成功:" + result);
}
}
2. 动态构建表单数据
public RequestBody buildFormBody(Map<String, String> formData) {
FormBody.Builder builder = new FormBody.Builder();
for (Map.Entry<String, String> entry : formData.entrySet()) {
builder.add(entry.getKey(), entry.getValue());
}
return builder.build();
}
// 使用
Map<String, String> formData = new HashMap<>();
formData.put("username", "zhangsan");
formData.put("password", "123456");
RequestBody formBody = buildFormBody(formData);
4. Form Data vs JSON
| 特性 | Form Data | JSON |
|---|---|---|
| Content-Type | application/x-www-form-urlencoded | application/json |
| 数据格式 | key=value&key=value | {"key":"value"} |
| 适用场景 | 表单提交、登录 | API接口 |
| 嵌套数据 | 不支持 | 支持 |
5. 注意事项
- 编码:FormBody会自动进行URL编码
- Content-Type:OkHttp会自动设置,无需手动指定
- 特殊字符:会自动处理特殊字符的编码
- 空值:空字符串会被正常发送
2.6 如何处理响应(Response)和响应头?
答案:
处理OkHttp响应需要了解Response的各个组成部分,包括响应体、响应头、状态码等。
1. 基本响应处理
try (Response response = client.newCall(request).execute()) {
if (response.isSuccessful()) {
String body = response.body().string();
Log.d("OkHttp", "响应内容:" + body);
} else {
Log.d("OkHttp", "请求失败,状态码:" + response.code());
}
}
2. 获取响应信息
Response response = client.newCall(request).execute();
// 状态码和消息
int code = response.code();
String message = response.message();
// 响应头
String contentType = response.header("Content-Type");
Headers headers = response.headers();
for (int i = 0; i < headers.size(); i++) {
Log.d("OkHttp", headers.name(i) + ": " + headers.value(i));
}
// 响应体
ResponseBody body = response.body();
String bodyString = body.string();
byte[] bodyBytes = body.bytes();
InputStream bodyStream = body.byteStream();
3. 处理不同类型的响应
(1)JSON响应
if (response.isSuccessful()) {
String json = response.body().string();
Gson gson = new Gson();
User user = gson.fromJson(json, User.class);
}
(2)文本响应
if (response.isSuccessful()) {
String text = response.body().string();
}
(3)二进制响应(文件)
if (response.isSuccessful()) {
byte[] bytes = response.body().bytes();
// 保存文件
}
4. 错误处理
try (Response response = client.newCall(request).execute()) {
if (response.isSuccessful()) {
String body = response.body().string();
} else {
int code = response.code();
if (code >= 400 && code < 500) {
Log.d("OkHttp", "客户端错误:" + code);
} else if (code >= 500) {
Log.d("OkHttp", "服务器错误:" + code);
}
}
} catch (IOException e) {
Log.d("OkHttp", "网络错误:" + e.getMessage());
}
7. 注意事项
- ResponseBody只能读取一次:读取后流会关闭
- 必须关闭Response:使用try-with-resources自动关闭
- 大文件处理:使用流式读取,避免内存溢出
- 字符编码:注意响应体的字符编码,默认UTF-8
- 响应头大小写不敏感:HTTP header名称不区分大小写
- 多个值:某些header可能有多个值,使用
headers.values()
第三章:OkHttp 异步请求(8 题)
3.1 OkHttp的同步请求和异步请求的区别是什么?
答案:
OkHttp支持两种请求方式:同步请求和异步请求,它们在使用场景和执行机制上有重要区别。
1. 执行方式
同步请求:
// 阻塞当前线程,直到响应返回
Response response = client.newCall(request).execute();
String body = response.body().string();
异步请求:
// 非阻塞,立即返回,通过回调处理响应
client.newCall(request).enqueue(new Callback() {
@Override
public void onResponse(Call call, Response response) throws IOException {
// 处理响应
}
@Override
public void onFailure(Call call, IOException e) {
// 处理错误
}
});
2. 主要区别
| 特性 | 同步请求 | 异步请求 |
|---|---|---|
| 执行线程 | 当前线程(阻塞) | 后台线程(非阻塞) |
| 返回值 | 直接返回Response | 通过回调返回 |
| 适用场景 | 后台线程、单元测试 | UI线程、主线程 |
| 性能影响 | 阻塞线程 | 不阻塞线程 |
| 错误处理 | try-catch | onFailure回调 |
| 取消请求 | 较困难 | 容易(call.cancel()) |
3. 线程模型详解
同步请求的线程执行流程:
调用线程(主线程/后台线程)
↓
execute() 调用
↓
【阻塞】在当前线程执行网络IO
├─ 建立TCP连接(当前线程)
├─ 发送HTTP请求(当前线程)
├─ 等待服务器响应(当前线程阻塞)
└─ 接收响应数据(当前线程)
↓
返回Response(当前线程)
详细说明:
- 调用线程:在哪个线程调用
execute(),就在哪个线程执行网络请求 - 网络IO:所有网络操作(连接、发送、接收)都在调用线程执行
- 阻塞:网络IO期间,调用线程被阻塞,无法执行其他操作
- 返回:Response在调用线程返回
异步请求的线程执行流程:
主线程(UI线程)
↓
enqueue() 调用
↓
【立即返回】不阻塞主线程
↓
Dispatcher线程池(后台线程)
├─ 线程1:执行请求1
├─ 线程2:执行请求2
└─ 线程N:执行请求N
↓
【在后台线程执行网络IO】
├─ 建立TCP连接(后台线程)
├─ 发送HTTP请求(后台线程)
├─ 等待服务器响应(后台线程)
└─ 接收响应数据(后台线程)
↓
回调执行(后台线程)
├─ onResponse() 在后台线程执行
└─ onFailure() 在后台线程执行
↓
【注意】更新UI需要切换到主线程
详细说明:
- 调用线程:
enqueue()在主线程调用,立即返回,不阻塞 - 网络IO线程:实际网络请求在Dispatcher管理的后台线程池中执行
- 回调线程:
onResponse()和onFailure()在后台线程执行 - UI更新:回调在后台线程,更新UI需要切换到主线程(使用
runOnUiThread()或Handler)
4. 使用场景
同步请求适用于:
- 后台线程执行
- 单元测试
- 需要立即获取结果的场景
- 批量请求处理
异步请求适用于:
- UI线程(Android主线程)
- 需要保持界面响应
- 长时间运行的请求
- 需要取消的请求
5. 代码示例对比
同步请求:
// ⚠️ 必须在后台线程执行
new Thread(() -> {
try {
Response response = client.newCall(request).execute();
String body = response.body().string();
// 更新UI需要切换到主线程
runOnUiThread(() -> {
textView.setText(body);
});
} catch (IOException e) {
e.printStackTrace();
}
}).start();
异步请求:
// 可以在主线程调用,立即返回
client.newCall(request).enqueue(new Callback() {
@Override
public void onResponse(Call call, Response response) throws IOException {
String body = response.body().string();
// ⚠️ 回调在后台线程,需要切换到主线程更新UI
runOnUiThread(() -> {
textView.setText(body);
});
}
@Override
public void onFailure(Call call, IOException e) {
runOnUiThread(() -> {
Toast.makeText(MainActivity.this, "网络错误", Toast.LENGTH_SHORT).show();
});
}
});
6. 线程执行总结
| 操作 | 同步请求 | 异步请求 |
|---|---|---|
| 调用线程 | 在哪个线程调用execute() | 主线程调用enqueue() |
| 网络IO线程 | 在调用线程执行(阻塞) | 在Dispatcher后台线程池执行 |
| 回调线程 | 无回调,直接返回 | 在后台线程执行回调 |
| UI更新 | 需要切换到主线程 | 需要切换到主线程 |
| 线程阻塞 | 阻塞调用线程 | 不阻塞调用线程 |
同步请求注意事项:
- ⚠️ 不能在Android主线程执行(会抛出NetworkOnMainThreadException)
- ⚠️ 会阻塞当前线程:网络IO期间线程无法执行其他操作
- ✓ 代码逻辑更直观
- ✓ 适合批量处理
异步请求注意事项:
- ✓ 可以在主线程执行:enqueue()立即返回,不阻塞
- ✓ 不阻塞主线程:网络IO在后台线程执行
- ⚠️ 回调在后台线程:onResponse/onFailure在Dispatcher线程池执行
- ⚠️ 更新UI需要切换线程:回调中更新UI必须使用runOnUiThread()或Handler
- ✓ 可以轻松取消请求
总结:
- 同步请求:在调用线程执行网络IO,会阻塞线程,适合后台线程
- 异步请求:在后台线程执行网络IO,不阻塞主线程,但回调也在后台线程,需要切换线程更新UI
3.2 如何使用OkHttp发送异步请求?
答案:
使用OkHttp发送异步请求使用enqueue()方法,通过Callback回调处理响应。
1. 基本用法
Request request = new Request.Builder()
.url("https://api.example.com/data")
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onResponse(Call call, Response response) throws IOException {
// 请求成功
if (response.isSuccessful()) {
String body = response.body().string();
Log.d("OkHttp", "响应:" + body);
} else {
Log.d("OkHttp", "请求失败:" + response.code());
}
}
@Override
public void onFailure(Call call, IOException e) {
// 请求失败(网络错误等)
e.printStackTrace();
}
});
2. 注意事项
- 回调在后台线程:
onResponse和onFailure都在后台线程执行,更新UI需要切换线程 - ResponseBody只能读取一次:读取后流会关闭
- 及时关闭Response:避免资源泄漏
Android中使用示例请参考3.3题目的"在Android中使用"部分。
3.3 Callback回调的使用方法是什么?
答案:
Callback是OkHttp异步请求的回调接口,用于处理请求的响应和错误。
1. Callback接口定义
public interface Callback {
void onResponse(Call call, Response response) throws IOException;
void onFailure(Call call, IOException e);
}
2. 基本使用
Callback callback = new Callback() {
@Override
public void onResponse(Call call, Response response) throws IOException {
// 请求成功时调用
// call: 请求的Call对象
// response: 响应对象
}
@Override
public void onFailure(Call call, IOException e) {
// 请求失败时调用
// call: 请求的Call对象
// e: 异常信息
}
};
client.newCall(request).enqueue(callback);
3. 在Android中使用
client.newCall(request).enqueue(new Callback() {
@Override
public void onResponse(Call call, Response response) throws IOException {
if (response.isSuccessful()) {
String body = response.body().string();
// ⚠️ 回调在后台线程,需要切换到主线程更新UI
runOnUiThread(() -> {
textView.setText(body);
});
}
}
@Override
public void onFailure(Call call, IOException e) {
runOnUiThread(() -> {
Toast.makeText(MainActivity.this, "网络错误", Toast.LENGTH_SHORT).show();
});
}
});
4. 注意事项
- 线程:回调在后台线程执行,更新UI需要切换线程
- 异常处理:onResponse可能抛出IOException,需要处理
- 资源管理:及时关闭Response,避免资源泄漏
- 取消检查:在onFailure中检查call.isCanceled()
3.4 如何处理异步请求的错误?
答案:
异步请求的错误处理需要在Callback的onFailure和onResponse方法中进行。
1. 错误类型
(1)网络错误(onFailure)
- 连接超时
- 连接失败
- 请求被取消
- DNS解析失败
(2)HTTP错误(onResponse)
- 4xx客户端错误(400, 401, 403, 404等)
- 5xx服务器错误(500, 502, 503等)
2. 处理网络错误(onFailure)
@Override
public void onFailure(Call call, IOException e) {
if (call.isCanceled()) return;
String errorMessage = "网络错误";
if (e instanceof SocketTimeoutException) {
errorMessage = "请求超时";
} else if (e instanceof ConnectException) {
errorMessage = "网络连接失败";
} else if (e instanceof UnknownHostException) {
errorMessage = "无法解析主机名";
}
// 显示错误提示(需要切换到主线程)
}
3. 处理HTTP错误(onResponse)
@Override
public void onResponse(Call call, Response response) throws IOException {
if (response.isSuccessful()) {
// 2xx状态码,处理成功响应
String body = response.body().string();
} else {
// 非2xx状态码,处理HTTP错误
int code = response.code();
if (code >= 400 && code < 500) {
// 客户端错误(401未授权、404未找到等)
} else if (code >= 500) {
// 服务器错误(500内部错误、502网关错误等)
}
}
}
4. 注意事项
- 检查isCanceled():避免处理已取消的请求
- 线程切换:Android中需要在主线程更新UI
- 错误信息:提供用户友好的错误提示
- 区分错误类型:网络错误(onFailure)和HTTP错误(onResponse中非2xx)
3.5 如何取消异步请求?
答案:
取消异步请求使用Call.cancel()方法,可以随时取消正在进行的请求。
1. 基本用法
Call call = client.newCall(request);
call.enqueue(callback);
// 取消请求
call.cancel();
// 检查请求状态
if (call.isCanceled()) {
// 请求已取消
}
2. Android中使用(Activity生命周期)
public class MainActivity extends AppCompatActivity {
private Call currentCall;
private void loadData() {
currentCall = client.newCall(request);
currentCall.enqueue(callback);
}
@Override
protected void onDestroy() {
super.onDestroy();
if (currentCall != null) {
currentCall.cancel();
}
}
}
3. 使用Tag取消请求
// 设置Tag并取消
Request request = new Request.Builder()
.url("https://api.example.com/data")
.tag("user-request")
.build();
client.dispatcher().cancel("user-request");
4. 注意事项
- 已执行的请求:已完成的请求无法取消
- 检查isCanceled():在回调中检查是否被取消
- 资源清理:取消请求后及时清理资源
- 生命周期管理:在Activity/Fragment销毁时取消请求
- 避免内存泄漏:取消请求后清空引用
3.6 如何管理多个异步请求?
答案:
管理多个异步请求需要跟踪所有活动的请求,并能够统一控制它们。
1. 使用List管理请求
public class RequestManager {
private final List<Call> activeCalls = new ArrayList<>();
public void executeRequest(Request request) {
Call call = client.newCall(request);
activeCalls.add(call);
call.enqueue(new Callback() {
@Override
public void onResponse(Call call, Response response) throws IOException {
activeCalls.remove(call);
// 处理响应
}
@Override
public void onFailure(Call call, IOException e) {
activeCalls.remove(call);
// 处理错误
}
});
}
public void cancelAll() {
for (Call call : activeCalls) {
if (!call.isExecuted()) {
call.cancel();
}
}
activeCalls.clear();
}
}
2. 使用Dispatcher管理
// 使用Dispatcher管理请求
Dispatcher dispatcher = client.dispatcher();
dispatcher.cancelAll(); // 取消所有请求
dispatcher.cancel("tag-name"); // 取消特定Tag的请求
3. 注意事项
- 线程安全:使用ConcurrentHashMap或同步机制
- 及时清理:请求完成后从集合中移除
- 生命周期管理:在Activity/Fragment销毁时取消所有请求
- 避免重复请求:相同tag的请求应该先取消旧的
3.7 异步请求的线程模型是什么?
答案:
OkHttp异步请求使用Dispatcher管理线程池,确保请求在后台线程执行。
1. Dispatcher的作用
Dispatcher管理请求的执行:
- 运行队列:正在执行的请求
- 等待队列:等待执行的请求
- 线程池:执行请求的后台线程
默认配置:
- 最大并发请求数:64
- 每个主机的最大并发数:5
- 线程池大小:无限制(根据需要创建)
2. 配置Dispatcher
Dispatcher dispatcher = new Dispatcher();
dispatcher.setMaxRequests(100); // 最大并发请求数
dispatcher.setMaxRequestsPerHost(10); // 每个主机最大并发数
OkHttpClient client = new OkHttpClient.Builder()
.dispatcher(dispatcher)
.build();
3. 线程模型总结
- enqueue():主线程调用,立即返回,不阻塞
- 网络请求:在Dispatcher管理的后台线程池执行
- Callback:在后台线程执行(onResponse/onFailure)
- UI更新:回调在后台线程,需要切换到主线程更新UI
详细的线程执行流程请参考3.1题目的"线程模型详解"部分。
3.8 如何避免异步请求的内存泄漏?
答案:
异步请求可能导致内存泄漏,需要正确管理Call和Callback的生命周期。
1. 常见内存泄漏场景
(1)持有Activity引用
// ❌ 错误:Callback持有Activity引用
client.newCall(request).enqueue(new Callback() {
@Override
public void onResponse(Call call, Response response) {
// 持有Activity的隐式引用,导致内存泄漏
textView.setText("结果");
}
});
(2)未取消请求
// ❌ 错误:Activity销毁时未取消请求
private Call call;
call = client.newCall(request);
call.enqueue(callback);
// Activity销毁时call仍持有引用,导致内存泄漏
2. 解决方案
(1)使用弱引用
private static class WeakCallback implements Callback {
private final WeakReference<MainActivity> activityRef;
@Override
public void onResponse(Call call, Response response) throws IOException {
MainActivity activity = activityRef.get();
if (activity != null && !call.isCanceled()) {
activity.runOnUiThread(() -> {
activity.textView.setText(response.body().string());
});
}
}
@Override
public void onFailure(Call call, IOException e) {
// 处理错误
}
}
(2)在生命周期中取消请求
在Activity/Fragment销毁时取消请求,避免内存泄漏。具体实现请参考3.5题目的"Android中使用(Activity生命周期)"部分。
3. 注意事项
- 及时取消:Activity/Fragment销毁时取消请求
- 避免持有引用:Callback不要持有Activity/Fragment的强引用
- 使用弱引用:必要时使用WeakReference
- 检查isCanceled():回调中检查请求是否已取消
- 清理资源:请求完成后及时清理引用
第四章:OkHttp 请求配置(7 题)
4.1 如何设置连接超时(Connect Timeout)?
答案:
连接超时是指建立TCP连接的最大等待时间。
1. 基本设置
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS) // 10秒连接超时
.build();
2. 超时时间说明
// 连接超时:建立TCP连接的最大时间
.connectTimeout(10, TimeUnit.SECONDS)
// 如果10秒内无法建立连接,抛出SocketTimeoutException
4. 不同场景的超时设置
// 快速请求(本地网络)
OkHttpClient fastClient = new OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.build();
// 普通请求
OkHttpClient normalClient = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.build();
// 慢速网络(移动网络)
OkHttpClient slowClient = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.build();
5. 动态配置
public class NetworkConfig {
public static OkHttpClient createClient(NetworkType type) {
OkHttpClient.Builder builder = new OkHttpClient.Builder();
switch (type) {
case WIFI:
builder.connectTimeout(5, TimeUnit.SECONDS);
break;
case MOBILE:
builder.connectTimeout(15, TimeUnit.SECONDS);
break;
case SLOW:
builder.connectTimeout(30, TimeUnit.SECONDS);
break;
}
return builder.build();
}
}
注意事项:
- 连接超时过短可能导致频繁超时
- 连接超时过长可能导致长时间等待
- 建议根据网络环境动态调整
4.2 如何设置读取超时(Read Timeout)?
答案:
读取超时是指从服务器读取数据的最大等待时间。
1. 基本设置
OkHttpClient client = new OkHttpClient.Builder()
.readTimeout(30, TimeUnit.SECONDS) // 30秒读取超时
.build();
2. 超时时间说明
// 读取超时:从服务器读取响应数据的最大时间
.readTimeout(30, TimeUnit.SECONDS)
// 如果30秒内没有数据可读,抛出SocketTimeoutException
3. 不同场景的设置
// 小数据量请求
OkHttpClient smallDataClient = new OkHttpClient.Builder()
.readTimeout(10, TimeUnit.SECONDS)
.build();
// 大数据量请求(文件下载)
OkHttpClient largeDataClient = new OkHttpClient.Builder()
.readTimeout(60, TimeUnit.SECONDS)
.build();
// 流式数据
OkHttpClient streamClient = new OkHttpClient.Builder()
.readTimeout(0, TimeUnit.SECONDS) // 0表示不超时
.build();
注意事项:
- 大文件下载需要更长的读取超时
- 流式数据可能需要设置0(不超时)
- 读取超时应该大于连接超时
4.3 如何设置写入超时(Write Timeout)?
答案:
写入超时是指向服务器发送数据的最大等待时间。
1. 基本设置
OkHttpClient client = new OkHttpClient.Builder()
.writeTimeout(30, TimeUnit.SECONDS) // 30秒写入超时
.build();
2. 不同场景的设置
// 小数据上传
OkHttpClient smallUploadClient = new OkHttpClient.Builder()
.writeTimeout(10, TimeUnit.SECONDS)
.build();
// 大文件上传
OkHttpClient largeUploadClient = new OkHttpClient.Builder()
.writeTimeout(120, TimeUnit.SECONDS) // 2分钟
.build();
注意事项:
- 文件上传需要更长的写入超时
- 写入超时应该根据数据大小调整
4.4 如何设置重试机制?
答案:
OkHttp默认不自动重试,需要手动实现或使用拦截器。
1. 使用拦截器实现重试
public class RetryInterceptor implements Interceptor {
private final int maxRetries;
public RetryInterceptor(int maxRetries) {
this.maxRetries = maxRetries;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = null;
IOException exception = null;
// 重试逻辑
for (int i = 0; i <= maxRetries; i++) {
try {
response = chain.proceed(request);
if (response.isSuccessful()) {
return response;
}
} catch (IOException e) {
exception = e;
// 判断是否应该重试
if (!shouldRetry(e, i)) {
throw e;
}
}
// 等待后重试
if (i < maxRetries) {
try {
Thread.sleep(1000 * (i + 1)); // 递增等待时间
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException("重试被中断", e);
}
}
}
if (response != null) {
return response;
}
throw exception;
}
private boolean shouldRetry(IOException e, int retryCount) {
// 网络错误可以重试
if (e instanceof SocketTimeoutException) {
return true;
}
if (e instanceof ConnectException) {
return true;
}
// 其他错误不重试
return false;
}
}
// 使用
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new RetryInterceptor(3))
.build();
2. 条件重试
public class ConditionalRetryInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
int retryCount = 0;
int maxRetries = 3;
// 只对特定状态码重试
while (!response.isSuccessful() &&
response.code() == 503 &&
retryCount < maxRetries) {
retryCount++;
response.close();
// 等待后重试
try {
Thread.sleep(1000 * retryCount);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
response = chain.proceed(request);
}
return response;
}
}
注意事项:
- 重试应该针对可恢复的错误(网络错误、5xx错误)
- 不应该对4xx错误重试(客户端错误)
- 使用指数退避策略
- 避免无限重试
4.5 如何设置重定向策略?
答案:
OkHttp默认自动处理重定向,可以通过拦截器自定义重定向策略。
1. 默认行为
// OkHttp默认自动跟随重定向
// 最多跟随20次重定向
OkHttpClient client = new OkHttpClient();
// 自动处理301、302、303、307、308重定向
2. 禁用重定向
OkHttpClient client = new OkHttpClient.Builder()
.followRedirects(false) // 禁用自动重定向
.build();
3. 自定义重定向处理
public class RedirectInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
// 检查重定向
if (response.isRedirect()) {
String location = response.header("Location");
if (location != null) {
// 自定义重定向逻辑
HttpUrl newUrl = request.url().resolve(location);
if (newUrl != null) {
Request newRequest = request.newBuilder()
.url(newUrl)
.build();
response.close();
return chain.proceed(newRequest);
}
}
}
return response;
}
}
注意事项:
- 默认最多跟随20次重定向
- 可以禁用自动重定向
- 可以通过拦截器自定义重定向逻辑
4.6 如何设置代理(Proxy)?
答案:
OkHttp支持HTTP和SOCKS代理。
1. HTTP代理
Proxy proxy = new Proxy(Proxy.Type.HTTP,
new InetSocketAddress("proxy.example.com", 8080));
OkHttpClient client = new OkHttpClient.Builder()
.proxy(proxy)
.build();
2. SOCKS代理
Proxy proxy = new Proxy(Proxy.Type.SOCKS,
new InetSocketAddress("socks.example.com", 1080));
OkHttpClient client = new OkHttpClient.Builder()
.proxy(proxy)
.build();
3. 代理认证
Authenticator proxyAuthenticator = new Authenticator() {
@Override
public Request authenticate(Route route, Response response) throws IOException {
String credential = Credentials.basic("username", "password");
return response.request().newBuilder()
.header("Proxy-Authorization", credential)
.build();
}
};
OkHttpClient client = new OkHttpClient.Builder()
.proxy(proxy)
.proxyAuthenticator(proxyAuthenticator)
.build();
注意事项:
- 支持HTTP和SOCKS代理
- 需要代理认证时使用Authenticator
- 某些网络环境可能需要代理
4.7 如何配置OkHttpClient?
答案:
OkHttpClient的配置使用Builder模式,可以设置各种参数。
1. 完整配置示例
OkHttpClient client = new OkHttpClient.Builder()
// 超时设置
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
// 连接池
.connectionPool(new ConnectionPool(5, 5, TimeUnit.MINUTES))
// 拦截器
.addInterceptor(new LoggingInterceptor())
.addNetworkInterceptor(new HeaderInterceptor())
// 缓存
.cache(new Cache(new File(context.getCacheDir(), "http-cache"), 10 * 1024 * 1024))
// Cookie管理
.cookieJar(new CookieJar() {
// Cookie处理逻辑
})
// 代理
.proxy(proxy)
// 认证
.authenticator(authenticator)
// 事件监听
.eventListener(new EventListener() {
// 事件处理
})
// 协议
.protocols(Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1))
// 重定向
.followRedirects(true)
.followSslRedirects(true)
.build();
2. 单例模式配置
public class OkHttpManager {
private static OkHttpClient instance;
public static OkHttpClient getInstance() {
if (instance == null) {
synchronized (OkHttpManager.class) {
if (instance == null) {
instance = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.addInterceptor(new LoggingInterceptor())
.build();
}
}
}
return instance;
}
}
3. 不同环境配置
public class OkHttpConfig {
public static OkHttpClient createDebugClient() {
return new OkHttpClient.Builder()
.addInterceptor(new HttpLoggingInterceptor()
.setLevel(HttpLoggingInterceptor.Level.BODY))
.build();
}
public static OkHttpClient createReleaseClient() {
return new OkHttpClient.Builder()
.addInterceptor(new HeaderInterceptor())
.build();
}
}
注意事项:
- 建议使用单例OkHttpClient
- 根据环境配置不同的Client
- 合理设置超时时间
- 添加必要的拦截器
第五章:OkHttp 文件操作(7 题)
5.1 如何使用OkHttp上传文件?
答案:
OkHttp上传文件采用MultipartBody机制,支持表单数据与文件混合上传。这种方式符合HTTP标准的multipart/form-data格式,适用于需要同时上传文件和其他字段的场景。
核心原理:
- MultipartBody将文件和其他字段编码为multipart格式
- 每个部分都有自己的Content-Type和Content-Disposition头
- 服务器端可以正确解析各个部分的内容
1. 上传单个文件
File file = new File("/path/to/file.jpg");
RequestBody fileBody = RequestBody.create(file, MediaType.parse("image/jpeg"));
RequestBody requestBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("file", "file.jpg", fileBody)
.addFormDataPart("description", "图片描述")
.build();
Request request = new Request.Builder()
.url("https://api.example.com/upload")
.post(requestBody)
.build();
try (Response response = client.newCall(request).execute()) {
if (response.isSuccessful()) {
String result = response.body().string();
}
}
2. 上传多个文件
MultipartBody.Builder builder = new MultipartBody.Builder().setType(MultipartBody.FORM);
builder.addFormDataPart("avatar", "avatar.jpg",
RequestBody.create(new File("/path/to/avatar.jpg"), MediaType.parse("image/jpeg")));
builder.addFormDataPart("cover", "cover.jpg",
RequestBody.create(new File("/path/to/cover.jpg"), MediaType.parse("image/jpeg")));
builder.addFormDataPart("title", "标题");
Request request = new Request.Builder()
.url("https://api.example.com/upload")
.post(builder.build())
.build();
注意事项:
- 内存管理:大文件上传时注意内存占用,考虑使用流式上传
- MIME类型:根据文件类型正确设置MediaType,如image/jpeg、application/pdf等
- 进度监听:对于大文件,建议实现上传进度监听,提升用户体验
5.2 如何使用OkHttp下载文件?
答案:
OkHttp下载文件的核心是将响应体的字节流写入本地文件。有两种方式:流式下载和一次性读取,根据文件大小选择合适的方案。
下载流程说明:
- 发送GET请求获取文件资源
- 从ResponseBody获取输入流
- 创建本地文件输出流
- 将数据从输入流复制到输出流
- 关闭所有流资源
1. 流式下载(推荐)
Request request = new Request.Builder()
.url("https://example.com/file.pdf")
.build();
try (Response response = client.newCall(request).execute()) {
if (response.isSuccessful() && response.body() != null) {
InputStream inputStream = response.body().byteStream();
FileOutputStream outputStream = new FileOutputStream("file.pdf");
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
outputStream.close();
}
}
2. 一次性读取(仅小文件)
try (Response response = client.newCall(request).execute()) {
if (response.isSuccessful()) {
byte[] bytes = response.body().bytes();
FileOutputStream fos = new FileOutputStream("file.pdf");
fos.write(bytes);
fos.close();
}
}
注意事项:
- 流式下载:大文件必须使用流式下载,避免OOM
- 响应检查:确保响应成功(isSuccessful())后再处理文件
- 资源管理:使用try-with-resources确保流正确关闭,防止资源泄漏
- 文件路径:使用应用内部存储或外部存储的合适目录,注意权限管理
5.3 如何实现文件上传进度监听?
答案:
文件上传进度监听通过自定义RequestBody实现。在writeTo()方法中,每次写入数据后计算已上传字节数,通过回调接口通知外部进度变化。
实现原理:
- 自定义RequestBody包装原始文件
- 重写writeTo()方法,在写入过程中统计已上传字节数
- 通过回调接口实时通知进度
- 使用Okio进行流式处理,避免内存问题
1. 自定义RequestBody
public class ProgressRequestBody extends RequestBody {
private final File file;
private final String contentType;
private final ProgressListener listener;
public interface ProgressListener {
void onProgress(long bytesWritten, long contentLength, boolean done);
}
public ProgressRequestBody(File file, String contentType, ProgressListener listener) {
this.file = file;
this.contentType = contentType;
this.listener = listener;
}
@Override
public MediaType contentType() {
return MediaType.parse(contentType);
}
@Override
public long contentLength() throws IOException {
return file.length();
}
@Override
public void writeTo(BufferedSink sink) throws IOException {
try (Source source = Okio.source(file)) {
long totalBytes = file.length();
long writtenBytes = 0;
Buffer buffer = new Buffer();
long read;
while ((read = source.read(buffer, 8192)) != -1) {
sink.write(buffer, read);
writtenBytes += read;
listener.onProgress(writtenBytes, totalBytes, false);
}
listener.onProgress(writtenBytes, totalBytes, true);
}
}
}
2. 使用进度监听
File file = new File("/path/to/file.jpg");
ProgressRequestBody requestBody = new ProgressRequestBody(file, "image/jpeg",
(bytesWritten, contentLength, done) -> {
int progress = (int) (bytesWritten * 100 / contentLength);
runOnUiThread(() -> progressBar.setProgress(progress));
}
);
Request request = new Request.Builder()
.url("https://api.example.com/upload")
.post(requestBody)
.build();
注意事项:
- 线程安全:进度回调在后台线程执行,更新UI必须切换到主线程
- 性能考虑:可以限制回调频率,避免过于频繁的UI更新(如每1%更新一次)
- 超时设置:大文件上传需要设置较长的超时时间
- 错误处理:监听过程中可能发生网络中断,需要妥善处理异常
5.4 如何实现文件下载进度监听?
答案:
文件下载进度监听通过自定义ResponseBody配合拦截器实现。使用装饰器模式包装原始ResponseBody,在数据读取过程中统计已下载字节数。
实现原理:
- 自定义ResponseBody包装原始的ResponseBody
- 使用ForwardingSource装饰Source,拦截read()操作
- 每次读取数据时统计已读取字节数并回调
- 通过拦截器将自定义ResponseBody注入到响应链中
1. 自定义ResponseBody
public class ProgressResponseBody extends ResponseBody {
private final ResponseBody responseBody;
private final ProgressListener listener;
private BufferedSource bufferedSource;
public interface ProgressListener {
void onProgress(long bytesRead, long contentLength, boolean done);
}
public ProgressResponseBody(ResponseBody responseBody, ProgressListener listener) {
this.responseBody = responseBody;
this.listener = listener;
}
@Override
public MediaType contentType() {
return responseBody.contentType();
}
@Override
public long contentLength() {
return responseBody.contentLength();
}
@Override
public BufferedSource source() {
if (bufferedSource == null) {
bufferedSource = Okio.buffer(source(responseBody.source()));
}
return bufferedSource;
}
private Source source(Source source) {
return new ForwardingSource(source) {
long totalBytesRead = 0L;
@Override
public long read(Buffer sink, long byteCount) throws IOException {
long bytesRead = super.read(sink, byteCount);
totalBytesRead += bytesRead != -1 ? bytesRead : 0;
listener.onProgress(totalBytesRead, responseBody.contentLength(), bytesRead == -1);
return bytesRead;
}
};
}
}
2. 使用拦截器
public class ProgressInterceptor implements Interceptor {
private final ProgressResponseBody.ProgressListener listener;
public ProgressInterceptor(ProgressResponseBody.ProgressListener listener) {
this.listener = listener;
}
@Override
public Response intercept(Chain chain) throws IOException {
Response response = chain.proceed(chain.request());
return response.newBuilder()
.body(new ProgressResponseBody(response.body(), listener))
.build();
}
}
// 配置使用
OkHttpClient client = new OkHttpClient.Builder()
.addNetworkInterceptor(new ProgressInterceptor((bytesRead, contentLength, done) -> {
if (contentLength > 0) {
int progress = (int) (bytesRead * 100 / contentLength);
runOnUiThread(() -> progressBar.setProgress(progress));
}
}))
.build();
注意事项:
- 线程处理:进度回调在后台线程执行,更新UI必须切换到主线程
- 未知大小:contentLength可能为-1(服务器未返回Content-Length),需要做判断
- 性能优化:可以限制回调频率,避免过于频繁的进度更新
- 拦截器位置:使用addNetworkInterceptor确保能获取到完整的响应体大小
5.5 如何实现断点续传?
答案:
断点续传通过HTTP Range请求实现,从上次中断的位置继续下载。对于大文件下载,断点续传能够节省流量和时间,提升用户体验。
实现原理:
- 检查本地已下载的文件大小
- 设置Range请求头,指定从哪个字节开始下载
- 服务器返回206状态码(Partial Content)和部分内容
- 使用追加模式将新数据追加到已有文件
1. 基本实现
public void downloadWithResume(String url, File targetFile) throws IOException {
long downloadedBytes = targetFile.exists() ? targetFile.length() : 0;
Request request = new Request.Builder()
.url(url)
.header("Range", "bytes=" + downloadedBytes + "-")
.build();
try (Response response = client.newCall(request).execute()) {
if (response.code() == 206 && response.body() != null) {
FileOutputStream fos = new FileOutputStream(targetFile, true);
InputStream inputStream = response.body().byteStream();
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
fos.close();
}
}
}
2. 完整实现(含进度监听)
public void download(String url, File targetFile, ProgressListener listener) throws IOException {
long startByte = targetFile.exists() ? targetFile.length() : 0;
Request request = new Request.Builder()
.url(url)
.header("Range", "bytes=" + startByte + "-")
.build();
try (Response response = client.newCall(request).execute()) {
if (response.code() == 206) {
long totalBytes = Long.parseLong(
response.header("Content-Range").split("/")[1]
);
FileOutputStream fos = new FileOutputStream(targetFile, true);
InputStream inputStream = response.body().byteStream();
byte[] buffer = new byte[8192];
long downloadedBytes = startByte;
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
downloadedBytes += bytesRead;
if (listener != null) {
listener.onProgress(downloadedBytes, totalBytes);
}
}
fos.close();
} else if (response.code() == 200) {
targetFile.delete();
download(url, targetFile, listener);
}
}
}
注意事项:
- 服务器支持:需要服务器支持HTTP Range请求(检查206状态码)
- 文件追加:必须使用追加模式(append=true)打开文件
- 进度保存:实际应用中应保存下载进度到配置文件,避免重新计算
- 错误处理:网络中断后应能够从保存的进度位置继续
5.6 如何上传多文件?
答案:
多文件上传通过MultipartBody.Builder逐个添加文件实现。适用于相册上传、批量附件上传等场景,可以同时上传多个文件和其他表单字段。
实现说明:
- 使用MultipartBody.Builder构建多部分请求体
- 每个文件作为独立的FormDataPart添加
- 可以同时添加文本字段和其他元数据
- 支持不同类型的文件(图片、文档等)
1. 基本方法
MultipartBody.Builder builder = new MultipartBody.Builder().setType(MultipartBody.FORM);
builder.addFormDataPart("file1", "image1.jpg",
RequestBody.create(new File("/path/to/image1.jpg"), MediaType.parse("image/jpeg")));
builder.addFormDataPart("file2", "image2.jpg",
RequestBody.create(new File("/path/to/image2.jpg"), MediaType.parse("image/jpeg")));
builder.addFormDataPart("title", "标题");
Request request = new Request.Builder()
.url("https://api.example.com/upload")
.post(builder.build())
.build();
2. 动态上传多个文件(推荐)
public String uploadMultipleFiles(List<File> files, Map<String, String> fields) throws IOException {
MultipartBody.Builder builder = new MultipartBody.Builder().setType(MultipartBody.FORM);
for (int i = 0; i < files.size(); i++) {
File file = files.get(i);
builder.addFormDataPart("file" + i, file.getName(),
RequestBody.create(file, MediaType.parse(getMimeType(file))));
}
for (Map.Entry<String, String> entry : fields.entrySet()) {
builder.addFormDataPart(entry.getKey(), entry.getValue());
}
try (Response response = client.newCall(
new Request.Builder().url("https://api.example.com/upload")
.post(builder.build()).build()).execute()) {
if (response.isSuccessful()) {
return response.body().string();
}
throw new IOException("上传失败:" + response.code());
}
}
注意事项:
- 内存管理:多文件上传时注意总文件大小,避免内存溢出
- 服务器支持:确保服务器支持multipart/form-data格式
- 进度监听:可以为每个文件单独实现进度监听,更细致的用户体验
- 错误处理:部分文件上传失败时的处理策略(全部重试或跳过失败文件)
5.7 如何上传大文件?
答案:
大文件上传(如视频、压缩包等)需要特别注意内存管理和网络稳定性。主要有两种方式:流式上传和分块上传,根据服务器支持情况选择。
核心要点:
- 使用流式处理,避免将整个文件加载到内存
- 设置较长的超时时间,适应大文件上传时间
- 实现进度监听,让用户了解上传状态
- 处理网络中断,支持断点续传
1. 流式上传(推荐)
File file = new File("/path/to/large-file.zip");
RequestBody requestBody = new RequestBody() {
@Override
public MediaType contentType() {
return MediaType.parse("application/zip");
}
@Override
public long contentLength() throws IOException {
return file.length();
}
@Override
public void writeTo(BufferedSink sink) throws IOException {
try (Source source = Okio.source(file)) {
Buffer buffer = new Buffer();
long read;
while ((read = source.read(buffer, 8192)) != -1) {
sink.write(buffer, read);
}
}
}
};
OkHttpClient client = new OkHttpClient.Builder()
.writeTimeout(10, TimeUnit.MINUTES)
.build();
Request request = new Request.Builder()
.url("https://api.example.com/upload")
.post(requestBody)
.build();
2. 分块上传
public void uploadLargeFileInChunks(File file, int chunkSize) throws IOException {
long fileSize = file.length();
int totalChunks = (int) Math.ceil((double) fileSize / chunkSize);
for (int i = 0; i < totalChunks; i++) {
long start = i * chunkSize;
long end = Math.min(start + chunkSize, fileSize);
RandomAccessFile raf = new RandomAccessFile(file, "r");
raf.seek(start);
byte[] buffer = new byte[(int) (end - start)];
raf.read(buffer);
raf.close();
Request request = new Request.Builder()
.url("https://api.example.com/upload-chunk")
.post(RequestBody.create(buffer, MediaType.parse("application/octet-stream")))
.header("Content-Range", String.format("bytes %d-%d/%d", start, end - 1, fileSize))
.header("Chunk-Number", String.valueOf(i))
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("上传块失败:" + response.code());
}
}
}
}
3. 注意事项
- 内存管理:必须使用流式上传,避免将整个文件加载到内存导致OOM
- 超时设置:设置合理的写入超时时间(如10-30分钟),适应大文件上传时长
- 进度监听:实现上传进度监听,让用户了解上传状态,提升体验
- 错误处理:处理网络中断、超时等异常情况,支持重试机制
- 断点续传:对于超大文件,实现断点续传可以节省流量和时间
- 服务器限制:注意服务器的文件大小限制和上传时间限制
第二部分:OkHttp 高级特性
第六章:OkHttp 拦截器(Interceptor)(12 题)
6.1 拦截器(Interceptor)是什么?它的作用是什么?
答案:
拦截器(Interceptor)是OkHttp的核心机制,用于在请求发送前和响应返回后进行处理。
拦截器的定义:
- 一个接口,实现请求和响应的拦截处理
- 可以修改请求、修改响应、重试请求等
- 通过责任链模式串联多个拦截器
拦截器的作用:
-
请求处理
- 添加公共请求头
- 添加认证信息
- 修改请求参数
- 记录请求日志
-
响应处理
- 解析响应数据
- 统一错误处理
- 响应缓存
- 响应解密
-
功能扩展
- 请求重试
- 请求加密
- 性能监控
- 数据统计
拦截器接口:
public interface Interceptor {
Response intercept(Chain chain) throws IOException;
}
基本使用:
public class MyInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
return response;
}
}
总结: 拦截器是OkHttp的扩展机制,通过拦截器可以实现各种自定义功能,是OkHttp灵活性的核心。
6.2 拦截器的执行顺序是什么?
答案:
拦截器按照添加顺序执行,形成一条拦截器链。
1. 执行流程
请求方向:
Application Interceptors
↓
RetryAndFollowUpInterceptor(内置)
↓
BridgeInterceptor(内置)
↓
CacheInterceptor(内置)
↓
ConnectInterceptor(内置)
↓
Network Interceptors
↓
CallServerInterceptor(内置)
↓
网络请求
响应方向(反向):
CallServerInterceptor
↑
Network Interceptors
↑
ConnectInterceptor
↑
CacheInterceptor
↑
BridgeInterceptor
↑
RetryAndFollowUpInterceptor
↑
Application Interceptors
2. 拦截器类型
(1)Application Interceptor(应用拦截器)
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new LoggingInterceptor())
.build();
(2)Network Interceptor(网络拦截器)
OkHttpClient client = new OkHttpClient.Builder()
.addNetworkInterceptor(new NetworkInterceptor())
.build();
4. 注意事项
- 顺序很重要:拦截器的执行顺序影响处理结果
- chain.proceed():必须调用,否则请求不会发送
- 响应处理:在chain.proceed()之后处理响应
- 异常处理:可以捕获和处理异常
6.3 如何自定义拦截器?
答案:
自定义拦截器需要实现Interceptor接口,在intercept方法中处理请求和响应。
1. 基本实现
public class CustomInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request().newBuilder()
.header("Custom-Header", "value")
.build();
Response response = chain.proceed(request);
return response.newBuilder()
.header("Custom-Response-Header", "value")
.build();
}
}
2. 添加公共请求头
public class HeaderInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request().newBuilder()
.header("Authorization", "Bearer " + getToken())
.header("User-Agent", "MyApp/1.0")
.build();
return chain.proceed(request);
}
private String getToken() {
return "token123";
}
}
3. 日志拦截器
public class LoggingInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
long startTime = System.currentTimeMillis();
Response response = chain.proceed(chain.request());
long endTime = System.currentTimeMillis();
Log.d("OkHttp", "响应: " + response.code() + " 耗时: " + (endTime - startTime) + "ms");
return response;
}
}
4. 重试拦截器
public class RetryInterceptor implements Interceptor {
private final int maxRetries;
public RetryInterceptor(int maxRetries) {
this.maxRetries = maxRetries;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
IOException lastException = null;
for (int i = 0; i <= maxRetries; i++) {
try {
Response response = chain.proceed(request);
if (response.isSuccessful()) {
return response;
}
} catch (IOException e) {
lastException = e;
if (i < maxRetries && shouldRetry(e)) {
try {
Thread.sleep(1000 * (i + 1));
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new IOException("重试被中断", ie);
}
} else {
throw e;
}
}
}
throw lastException;
}
private boolean shouldRetry(IOException e) {
return e instanceof SocketTimeoutException || e instanceof ConnectException;
}
}
注意事项:
- 必须调用chain.proceed()
- 可以修改请求和响应
- 注意异常处理
- 考虑性能影响
6.4 应用拦截器(Application Interceptor)和网络拦截器(Network Interceptor)的区别是什么?
答案:
应用拦截器和网络拦截器在执行时机、访问权限和使用场景上有重要区别。
1. 执行时机
Application Interceptor:
- 在所有拦截器之前执行
- 即使从缓存返回也会执行
- 可以访问完整的请求和响应
Network Interceptor:
- 在网络层执行
- 只对实际网络请求执行
- 从缓存返回时不执行
2. 主要区别
| 特性 | Application Interceptor | Network Interceptor |
|---|---|---|
| 添加方法 | addInterceptor() | addNetworkInterceptor() |
| 执行时机 | 最早执行 | 网络层执行 |
| 缓存请求 | 会执行 | 不执行 |
| 访问连接 | 否 | 是(可以访问Connection) |
| 重定向 | 只看到最终请求 | 可以看到所有重定向 |
| 使用场景 | 日志、认证、请求修改 | 网络监控、重试 |
3. 代码示例
Application Interceptor:
public class AppInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request().newBuilder()
.header("Authorization", "Bearer " + token)
.build();
return chain.proceed(request);
}
}
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new AppInterceptor())
.build();
Network Interceptor:
public class NetworkInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Connection connection = chain.connection();
return chain.proceed(chain.request());
}
}
OkHttpClient client = new OkHttpClient.Builder()
.addNetworkInterceptor(new NetworkInterceptor())
.build();
4. 执行流程对比
Application Interceptor:
请求 → App Interceptor → 缓存检查 → 网络请求 → 响应 → App Interceptor
↑ ↑
即使从缓存返回也会执行
Network Interceptor:
请求 → 缓存检查 → Network Interceptor → 网络请求 → Network Interceptor → 响应
↑ ↑
只对实际网络请求执行
5. 使用建议
使用Application Interceptor:
- 添加公共请求头
- 添加认证信息
- 请求日志记录
- 请求参数修改
使用Network Interceptor:
- 网络性能监控
- 请求重试(网络错误)
- 网络层日志
- 连接信息访问
6. 注意事项
- 缓存影响:Application Interceptor会处理缓存响应
- 性能影响:Network Interceptor只对网络请求执行,性能更好
- 连接访问:只有Network Interceptor可以访问Connection
- 重定向:Network Interceptor可以看到所有重定向请求
6.5 如何使用拦截器添加公共请求头?
答案:
使用拦截器添加公共请求头是最佳实践,可以统一管理所有请求的头部信息。
1. 基本实现
public class HeaderInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request().newBuilder()
.header("Authorization", "Bearer " + getToken())
.header("User-Agent", "MyApp/1.0")
.build();
return chain.proceed(request);
}
private String getToken() {
return "token123";
}
}
2. 动态获取Token
public class AuthInterceptor implements Interceptor {
private final TokenProvider tokenProvider;
public AuthInterceptor(TokenProvider tokenProvider) {
this.tokenProvider = tokenProvider;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request.Builder builder = chain.request().newBuilder();
String token = tokenProvider.getToken();
if (token != null) {
builder.header("Authorization", "Bearer " + token);
}
return chain.proceed(builder.build());
}
}
3. 条件添加Header
public class ConditionalHeaderInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Request.Builder builder = request.newBuilder();
if (request.url().toString().contains("/api/")) {
builder.header("X-API-Version", "v1");
}
if ("POST".equals(request.method())) {
builder.header("Content-Type", "application/json");
}
return chain.proceed(builder.build());
}
}
注意事项:
- 使用addInterceptor()添加应用拦截器
- 可以覆盖请求中已设置的header
- 动态获取的值需要实时更新
6.6 如何使用拦截器添加Token认证?
答案:
使用拦截器添加Token认证可以统一处理所有请求的认证信息。
1. 基本实现
public class AuthInterceptor implements Interceptor {
private String token;
public void setToken(String token) {
this.token = token;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request.Builder builder = chain.request().newBuilder();
if (token != null) {
builder.header("Authorization", "Bearer " + token);
}
return chain.proceed(builder.build());
}
}
2. 处理Token过期
public class AuthInterceptor implements Interceptor {
private final TokenManager tokenManager;
public AuthInterceptor(TokenManager tokenManager) {
this.tokenManager = tokenManager;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request().newBuilder()
.header("Authorization", "Bearer " + tokenManager.getToken())
.build();
Response response = chain.proceed(request);
if (response.code() == 401) {
String newToken = tokenManager.refreshToken();
if (newToken != null) {
response.close();
return chain.proceed(request.newBuilder()
.header("Authorization", "Bearer " + newToken)
.build());
}
}
return response;
}
}
3. 使用Authenticator(推荐)
public class TokenAuthenticator implements Authenticator {
private final TokenManager tokenManager;
public TokenAuthenticator(TokenManager tokenManager) {
this.tokenManager = tokenManager;
}
@Override
public Request authenticate(Route route, Response response) throws IOException {
if (response.code() == 401) {
String newToken = tokenManager.refreshToken();
if (newToken != null) {
return response.request().newBuilder()
.header("Authorization", "Bearer " + newToken)
.build();
}
}
return null;
}
}
OkHttpClient client = new OkHttpClient.Builder()
.authenticator(new TokenAuthenticator(tokenManager))
.build();
注意事项:
- Token应该安全存储
- 处理Token刷新逻辑
- 避免无限重试
6.7 如何使用拦截器实现请求日志?
答案:
使用拦截器实现请求日志可以方便调试和监控。
1. 基本日志拦截器
public class LoggingInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
long startTime = System.currentTimeMillis();
Response response = chain.proceed(chain.request());
long endTime = System.currentTimeMillis();
Log.d("OkHttp", "响应: " + response.code() + " 耗时: " + (endTime - startTime) + "ms");
return response;
}
}
2. 使用HttpLoggingInterceptor(推荐)
HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.build();
日志级别:
NONE:不记录日志BASIC:记录请求和响应行HEADERS:记录请求和响应头BODY:记录请求和响应体(最详细)
注意事项:
- 生产环境应该禁用或使用BASIC级别
- 记录敏感信息需要脱敏
- 性能影响:详细日志可能影响性能
6.8 如何使用拦截器实现请求重试?
答案:
使用拦截器实现请求重试可以自动处理网络错误。
1. 基本重试拦截器
public class RetryInterceptor implements Interceptor {
private final int maxRetries;
public RetryInterceptor(int maxRetries) {
this.maxRetries = maxRetries;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
IOException lastException = null;
for (int i = 0; i <= maxRetries; i++) {
try {
Response response = chain.proceed(request);
if (response.isSuccessful()) {
return response;
}
} catch (IOException e) {
lastException = e;
if (i < maxRetries && shouldRetry(e)) {
try {
Thread.sleep(1000 * (i + 1));
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new IOException("重试被中断", ie);
}
} else {
throw e;
}
}
}
throw lastException;
}
private boolean shouldRetry(IOException e) {
return e instanceof SocketTimeoutException ||
e instanceof ConnectException ||
e instanceof UnknownHostException;
}
}
注意事项:
- 只对可恢复的错误重试
- 使用指数退避策略
- 避免无限重试
- 注意重试次数限制
6.9 如何使用拦截器实现请求缓存?
答案:
OkHttp内置了CacheInterceptor,也可以自定义缓存拦截器。
1. 使用内置缓存
File cacheDir = new File(context.getCacheDir(), "http-cache");
Cache cache = new Cache(cacheDir, 10 * 1024 * 1024);
OkHttpClient client = new OkHttpClient.Builder()
.cache(cache)
.build();
注意事项:
- 使用HTTP缓存标准
- 注意缓存大小限制
- 处理缓存失效
6.10 如何使用拦截器实现请求加密?
答案:
使用拦截器可以在发送前加密请求体。
1. 加密拦截器
public class EncryptionInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
RequestBody body = request.body();
if (body != null) {
RequestBody encryptedBody = RequestBody.create(
encrypt(body.toString()),
MediaType.parse("application/octet-stream")
);
request = request.newBuilder()
.method(request.method(), encryptedBody)
.header("Content-Encrypted", "true")
.build();
}
return chain.proceed(request);
}
private String encrypt(String data) {
return "encrypted_data";
}
}
注意事项:
- 选择合适的加密算法
- 注意性能影响
- 安全管理密钥
6.11 如何使用拦截器实现响应解密?
答案:
使用拦截器可以在收到响应后解密响应体。
1. 解密拦截器
public class DecryptionInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Response response = chain.proceed(chain.request());
if (response.header("Content-Encrypted") != null) {
String encryptedBody = response.body().string();
ResponseBody decryptedBody = ResponseBody.create(
decrypt(encryptedBody),
MediaType.parse("application/json")
);
return response.newBuilder()
.body(decryptedBody)
.removeHeader("Content-Encrypted")
.build();
}
return response;
}
private String decrypt(String data) {
return "decrypted_data";
}
}
注意事项:
- ResponseBody只能读取一次
- 需要重新创建ResponseBody
- 注意性能影响
6.12 拦截器的性能影响是什么?
答案:
拦截器会对性能产生影响,需要合理使用。
1. 性能影响
(1)执行时间
- 每个拦截器都会增加执行时间
- 复杂逻辑的拦截器影响更大
- 网络拦截器只对网络请求执行,影响较小
(2)内存占用
- 读取和修改请求/响应可能增加内存
- 大文件处理需要注意内存
(3)CPU使用
- 加密解密等操作消耗CPU
- 日志记录也有一定开销
2. 优化建议
(1)避免不必要的拦截器
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(DEBUG ? new LoggingInterceptor() : new EmptyInterceptor())
.build();
(2)使用网络拦截器
OkHttpClient client = new OkHttpClient.Builder()
.addNetworkInterceptor(new NetworkInterceptor())
.build();
(3)异步处理
public class AsyncInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Response response = chain.proceed(chain.request());
new Handler(Looper.getMainLooper()).post(() -> {
// 日志记录、统计等
});
return response;
}
}
注意事项:
- 合理使用拦截器数量
- 避免在拦截器中执行耗时操作
- 生产环境禁用详细日志
- 监控拦截器性能
第七章:OkHttp 连接池(ConnectionPool)(8 题)
7.1 连接池(ConnectionPool)的作用是什么?
答案:
连接池用于管理和复用TCP连接,减少连接建立的开销。
连接池的作用:
-
连接复用
- 复用已建立的TCP连接
- 避免频繁建立和关闭连接
- 减少TCP握手开销
-
性能提升
- 减少连接建立时间
- 提高请求响应速度
- 降低网络延迟
-
资源管理
- 管理连接的生命周期
- 控制连接数量
- 自动清理空闲连接
-
HTTP/2.0支持
- 支持多路复用
- 单连接多请求
- 进一步提升性能
默认配置:
ConnectionPool pool = new ConnectionPool();
// 最大空闲连接数:5,保持时间:5分钟
性能影响:
- 连接复用可以减少**30-50%**的延迟
- 特别适合频繁请求同一主机的场景
- HTTP/2.0多路复用效果更明显
7.2 连接池的工作原理是什么?
答案:
连接池通过维护一个连接队列来管理TCP连接的生命周期。
1. 工作原理
请求到达
↓
检查连接池
↓
有可用连接?
├─ 是 → 复用连接 → 执行请求
└─ 否 → 创建新连接 → 加入连接池 → 执行请求
↓
请求完成
↓
连接空闲
↓
定时清理(超过保持时间或达到最大连接数)
2. 连接匹配
连接池通过Route匹配连接:
- 主机名(Host)
- 端口号(Port)
- 协议(HTTP/HTTPS)
- 代理(Proxy)
3. 连接状态
(1)空闲连接
- 请求完成后连接空闲
- 保持在连接池中
- 等待下次复用
(2)活跃连接
- 正在使用的连接
- 不能复用
- 使用完成后变为空闲
(3)清理连接
- 空闲时间超过保持时间
- 连接池达到最大连接数
- 连接不可用
4. 内部实现
// OkHttp内部实现(简化)
class ConnectionPool {
private final Deque<RealConnection> connections = new ArrayDeque<>();
// 获取连接
RealConnection get(Address address) {
for (RealConnection connection : connections) {
if (connection.isEligible(address)) {
return connection; // 复用连接
}
}
return null; // 没有可用连接
}
// 添加连接
void put(RealConnection connection) {
connections.add(connection);
}
// 清理空闲连接
void cleanup() {
// 清理超时或过多的连接
}
}
5. 连接复用条件
- 相同Route:主机、端口、协议、代理都相同
- 连接可用:连接未关闭且可用
- 未达到上限:连接池未满
总结: 连接池通过维护连接队列,实现连接的复用和管理,显著提升网络性能。
7.3 如何配置连接池的大小?
答案:
通过ConnectionPool构造函数配置连接池的大小和保持时间。
1. 基本配置
ConnectionPool connectionPool = new ConnectionPool(
10, // 最大空闲连接数
5, // 保持时间
TimeUnit.MINUTES
);
OkHttpClient client = new OkHttpClient.Builder()
.connectionPool(connectionPool)
.build();
2. 不同场景的配置
// 小应用
ConnectionPool pool = new ConnectionPool(5, 5, TimeUnit.MINUTES);
// 中等应用
ConnectionPool pool = new ConnectionPool(10, 5, TimeUnit.MINUTES);
// 大应用
ConnectionPool pool = new ConnectionPool(20, 5, TimeUnit.MINUTES);
3. 动态配置
public class ConnectionPoolManager {
public static ConnectionPool createPool(NetworkType type) {
switch (type) {
case WIFI:
return new ConnectionPool(10, 5, TimeUnit.MINUTES);
case MOBILE:
return new ConnectionPool(5, 3, TimeUnit.MINUTES);
default:
return new ConnectionPool(5, 5, TimeUnit.MINUTES);
}
}
}
5. 注意事项
- 连接数过多:占用系统资源(文件描述符、内存)
- 连接数过少:可能影响并发性能
- 保持时间过长:占用资源,可能连接已失效
- 保持时间过短:频繁创建连接,影响性能
最佳实践:
- 根据实际需求调整
- 监控连接池使用情况
- 使用单例OkHttpClient共享连接池
7.4 连接池如何管理连接的生命周期?
答案:
连接池通过定时清理机制管理连接的生命周期。
1. 连接生命周期阶段
(1)创建阶段
RealConnection connection = new RealConnection(...);
connection.connect();
(2)使用阶段
connection.socket().getOutputStream().write(data);
(3)空闲阶段
connectionPool.put(connection);
(4)清理阶段
connectionPool.cleanup();
connection.close();
2. 清理机制
(1)定时清理
class ConnectionPool {
private void cleanup() {
long now = System.nanoTime();
int idleConnectionCount = 0;
RealConnection longestIdleConnection = null;
long longestIdleDurationNs = Long.MIN_VALUE;
for (RealConnection connection : connections) {
long idleDurationNs = now - connection.idleAtNanos;
if (idleDurationNs > keepAliveDurationNs) {
connection.close();
connections.remove(connection);
} else {
idleConnectionCount++;
if (idleDurationNs > longestIdleDurationNs) {
longestIdleDurationNs = idleDurationNs;
longestIdleConnection = connection;
}
}
}
if (idleConnectionCount > maxIdleConnections) {
longestIdleConnection.close();
connections.remove(longestIdleConnection);
}
}
}
(2)清理触发条件
- 时间条件:空闲时间超过保持时间
- 数量条件:连接数超过最大连接数
- 连接失效:连接不可用
3. 连接状态管理
(1)空闲时间记录
connection.idleAtNanos = System.nanoTime();
(2)连接复用检查
if (connection.isHealthy()) {
// 复用连接
} else {
connection.close();
}
4. 完整生命周期
1. 创建连接
↓
2. 建立TCP连接
↓
3. 执行HTTP请求
↓
4. 请求完成,连接空闲
↓
5. 加入连接池
↓
6. 等待复用或清理
├─ 被复用 → 返回步骤3
└─ 超时/达到上限 → 关闭连接
5. 注意事项
- 及时清理:避免连接泄漏
- 健康检查:检查连接是否可用
- 资源释放:关闭连接释放资源
- 监控管理:监控连接池状态
7.5 连接池如何实现连接复用?
答案:
连接池通过匹配请求的Route来复用已存在的连接。
1. 连接复用条件
Route匹配:
- 相同主机名(Host)
- 相同端口号(Port)
- 相同协议(HTTP/HTTPS)
- 相同代理(Proxy)
连接可用:
- 连接未关闭
- 连接健康(Socket可用)
- 连接空闲或可复用
2. 复用流程
新请求到达
↓
提取Route信息(主机、端口、协议)
↓
在连接池中查找匹配的连接
↓
找到匹配连接?
├─ 是 → 检查连接是否可用
│ ├─ 可用 → 复用连接
│ └─ 不可用 → 移除连接,创建新连接
└─ 否 → 创建新连接
↓
执行请求
↓
请求完成,连接空闲
↓
加入连接池(如果未达到上限)
3. 代码实现(简化)
class ConnectionPool {
RealConnection get(Address address, Route route) {
for (RealConnection connection : connections) {
if (connection.isEligible(address, route)) {
if (connection.isHealthy()) {
return connection;
} else {
connections.remove(connection);
connection.close();
}
}
}
return null;
}
void put(RealConnection connection) {
if (connections.size() < maxIdleConnections) {
connections.add(connection);
} else {
connection.close();
}
}
}
4. HTTP/2.0多路复用
HTTP/1.1:
连接1 → 请求1 → 响应1 → 请求2 → 响应2
(串行,一个连接一个请求)
HTTP/2.0:
连接1 → 请求1 ─┐
→ 请求2 ─┼→ 并行处理 → 响应1
→ 请求3 ─┘ 响应2
响应3
(并行,一个连接多个请求)
5. 复用优势
- 减少延迟:避免TCP握手(约100-200ms)
- 提高性能:连接复用可提升30-50%性能
- 节省资源:减少连接数,节省系统资源
- HTTP/2.0:多路复用进一步提升性能
6. 注意事项
- 连接匹配:必须Route完全匹配才能复用
- 连接健康:复用前检查连接是否可用
- 连接上限:注意连接池大小限制
- 连接清理:及时清理不可用连接
7.6 连接池的清理机制是什么?
答案:
连接池通过后台线程定期清理空闲和过期的连接。
1. 清理机制
(1)定时清理
class ConnectionPool {
private void cleanup() {
long now = System.nanoTime();
long keepAliveDurationNs = TimeUnit.NANOSECONDS.convert(
keepAliveDuration, keepAliveTimeUnit);
int idleConnectionCount = 0;
RealConnection longestIdleConnection = null;
long longestIdleDurationNs = Long.MIN_VALUE;
synchronized (this) {
for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
RealConnection connection = i.next();
long idleDurationNs = now - connection.idleAtNanos;
if (idleDurationNs > keepAliveDurationNs) {
connection.close();
i.remove();
} else {
idleConnectionCount++;
if (idleDurationNs > longestIdleDurationNs) {
longestIdleDurationNs = idleDurationNs;
longestIdleConnection = connection;
}
}
}
if (idleConnectionCount > maxIdleConnections) {
longestIdleConnection.close();
connections.remove(longestIdleConnection);
}
}
}
}
2. 清理触发
(1)定时触发
- 后台线程定期执行清理
- 默认每5分钟执行一次
- 可以自定义清理间隔
(2)条件触发
- 添加新连接时检查
- 连接空闲时检查
- 达到上限时立即清理
3. 清理策略
(1)时间策略
- 空闲时间超过保持时间的连接被清理
- 默认保持时间:5分钟
(2)数量策略
- 连接数超过最大连接数时清理
- 优先清理最旧的连接
- 默认最大连接数:5
(3)健康策略
- 连接不可用时立即清理
- 连接已关闭时清理
4. 清理示例
// 场景1:空闲时间超过保持时间
连接A:空闲6分钟(超过5分钟) → 清理
连接B:空闲3分钟 → 保留
// 场景2:连接数超过上限
连接池:6个连接(超过5个) → 清理最旧的1个
// 场景3:连接不可用
连接C:Socket已关闭 → 立即清理
5. 注意事项
- 及时清理:避免连接泄漏
- 合理配置:根据需求配置保持时间和最大连接数
- 监控清理:监控清理频率和效果
- 资源释放:确保连接正确关闭
7.7 如何监控连接池的状态?
答案:
可以通过ConnectionPool的方法和EventListener监控连接池状态。
1. 使用ConnectionPool方法
ConnectionPool pool = client.connectionPool();
int idleConnectionCount = pool.idleConnectionCount();
2. 使用EventListener
public class ConnectionEventListener extends EventListener {
@Override
public void connectionAcquired(Call call, Connection connection) {
Log.d("OkHttp", "获取连接: " + connection.protocol());
}
@Override
public void connectionReleased(Call call, Connection connection) {
Log.d("OkHttp", "释放连接");
}
}
OkHttpClient client = new OkHttpClient.Builder()
.eventListener(new ConnectionEventListener())
.build();
4. 监控指标
- 空闲连接数:当前空闲的连接数
- 总连接数:连接池中的总连接数
- 连接复用率:连接被复用的比例
- 连接创建数:新创建的连接数
- 连接清理数:被清理的连接数
注意事项:
- 监控可能影响性能
- 生产环境谨慎使用详细监控
- 使用异步方式记录监控数据
7.8 连接池的性能优化策略是什么?
答案:
连接池的性能优化策略包括合理配置、监控管理和使用HTTP/2.0。
1. 配置优化
// 根据应用规模配置
ConnectionPool pool = new ConnectionPool(10, 5, TimeUnit.MINUTES);
2. 使用HTTP/2.0
OkHttpClient client = new OkHttpClient.Builder()
.protocols(Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1))
.build();
3. 单例OkHttpClient
public class OkHttpManager {
private static final OkHttpClient client = new OkHttpClient.Builder()
.connectionPool(new ConnectionPool(10, 5, TimeUnit.MINUTES))
.build();
public static OkHttpClient getClient() {
return client;
}
}
5. 最佳实践
- 使用单例:共享连接池
- 合理配置:根据需求调整
- 启用HTTP/2.0:提升性能
- 监控状态:及时发现问题
- 定期清理:避免连接泄漏
性能提升:
- 连接复用可减少30-50%延迟
- HTTP/2.0多路复用进一步提升
- 合理配置可优化20-30%性能
第八章:OkHttp 缓存机制(8 题)
8.1 OkHttp的缓存机制是什么?
答案:
OkHttp实现了HTTP标准的缓存机制,支持强制缓存和协商缓存。
1. 缓存机制概述
OkHttp的缓存机制遵循HTTP缓存标准(RFC 7234),包括:
- 强制缓存:Cache-Control、Expires
- 协商缓存:Last-Modified、ETag
- 缓存验证:If-Modified-Since、If-None-Match
- 缓存控制:no-cache、no-store等
2. 缓存类型
(1)强制缓存
- 服务器设置缓存有效期
- 有效期内直接返回缓存
- 不需要请求服务器
(2)协商缓存
- 服务器返回Last-Modified或ETag
- 客户端发送验证请求
- 服务器返回304或新资源
3. 缓存流程
请求到达
↓
检查缓存
↓
有缓存?
├─ 是 → 检查缓存有效性
│ ├─ 有效 → 返回缓存(强制缓存)
│ └─ 无效 → 发送验证请求
│ ├─ 304 Not Modified → 返回缓存(协商缓存)
│ └─ 200 OK → 返回新资源,更新缓存
└─ 否 → 请求网络 → 保存缓存 → 返回响应
4. 缓存存储
- 存储位置:磁盘文件系统
- 存储格式:响应头和响应体分开存储
- 存储大小:可配置(默认无限制,建议10-50MB)
5. 缓存优势
- 减少网络请求:直接使用缓存
- 提升响应速度:缓存响应更快
- 节省流量:减少数据传输
- 离线支持:有缓存时可以离线访问
总结: OkHttp的缓存机制遵循HTTP标准,自动管理缓存,对用户透明,显著提升性能和用户体验。
8.2 如何配置OkHttp缓存?
答案:
通过Cache类配置OkHttp缓存,需要指定缓存目录和大小。
1. 基本配置
// 创建缓存目录
File cacheDir = new File(context.getCacheDir(), "http-cache");
// 创建缓存(10MB)
Cache cache = new Cache(cacheDir, 10 * 1024 * 1024);
OkHttpClient client = new OkHttpClient.Builder()
.cache(cache)
.build();
2. 配置缓存大小
// 小应用:5MB
Cache smallCache = new Cache(cacheDir, 5 * 1024 * 1024);
// 中等应用:10MB
Cache mediumCache = new Cache(cacheDir, 10 * 1024 * 1024);
// 大应用:50MB
Cache largeCache = new Cache(cacheDir, 50 * 1024 * 1024);
3. Android中的配置
public class NetworkManager {
private static OkHttpClient client;
public static OkHttpClient getClient(Context context) {
if (client == null) {
// 创建缓存目录
File cacheDir = new File(context.getCacheDir(), "http-cache");
Cache cache = new Cache(cacheDir, 10 * 1024 * 1024);
client = new OkHttpClient.Builder()
.cache(cache)
.build();
}
return client;
}
}
4. 检查缓存配置
Cache cache = client.cache();
if (cache != null) {
// 缓存已配置
long size = cache.size();
long maxSize = cache.maxSize();
Log.d("OkHttp", "缓存大小: " + size + " / " + maxSize);
} else {
// 缓存未配置
Log.d("OkHttp", "缓存未启用");
}
5. 注意事项
- 目录权限:确保缓存目录有写权限
- 缓存大小:根据应用需求设置
- 定期清理:避免缓存过大
- 存储位置:使用应用缓存目录
8.3 缓存的工作原理是什么?
答案:
OkHttp的缓存遵循HTTP缓存标准,通过检查响应头决定是否缓存。
1. 缓存判断流程
收到响应
↓
检查响应头
↓
有Cache-Control或Expires?
├─ 是 → 检查缓存指令
│ ├─ max-age > 0 → 缓存(强制缓存)
│ ├─ no-cache → 不缓存,每次验证
│ └─ no-store → 不缓存
└─ 否 → 检查Last-Modified或ETag
├─ 有 → 协商缓存
└─ 无 → 不缓存
2. 强制缓存
服务器响应:
Cache-Control: max-age=3600
Expires: Wed, 21 Oct 2025 07:28:00 GMT
OkHttp处理:
- 计算过期时间
- 保存响应到缓存
- 下次请求时检查是否过期
- 未过期直接返回缓存
3. 协商缓存
首次请求:
请求 → 服务器
响应 ← Last-Modified: Wed, 21 Oct 2024 10:00:00 GMT
ETag: "abc123"
再次请求:
请求 → If-Modified-Since: Wed, 21 Oct 2024 10:00:00 GMT
If-None-Match: "abc123"
响应 ← 304 Not Modified(资源未修改)
或 200 OK(资源已修改)
4. 缓存验证
(1)时间验证(Last-Modified)
// 服务器返回
Last-Modified: Wed, 21 Oct 2024 10:00:00 GMT
// 下次请求
If-Modified-Since: Wed, 21 Oct 2024 10:00:00 GMT
// 服务器判断
if (资源修改时间 <= If-Modified-Since) {
return 304 Not Modified;
} else {
return 200 OK + 新资源;
}
(2)内容验证(ETag)
// 服务器返回
ETag: "abc123"
// 下次请求
If-None-Match: "abc123"
// 服务器判断
if (资源ETag == If-None-Match) {
return 304 Not Modified;
} else {
return 200 OK + 新资源;
}
5. 缓存存储
存储结构:
缓存目录/
├── 文件1(URL哈希值)
│ ├── 响应头
│ └── 响应体
├── 文件2
└── journal(日志文件)
6. 完整工作流程
1. 请求到达
↓
2. 检查缓存目录
↓
3. 找到缓存?
├─ 是 → 检查缓存有效性
│ ├─ 强制缓存有效 → 返回缓存
│ └─ 需要验证 → 发送条件请求
│ ├─ 304 → 返回缓存
│ └─ 200 → 更新缓存,返回新资源
└─ 否 → 请求网络
↓
收到响应
↓
检查缓存指令
↓
保存缓存(如果允许)
↓
返回响应
总结: OkHttp的缓存机制自动处理HTTP缓存标准,通过响应头判断缓存策略,实现高效的缓存管理。
8.4 如何实现强制缓存?
答案:
强制缓存通过Cache-Control和Expires响应头实现,OkHttp自动处理。
1. 服务器设置强制缓存
(1)使用Cache-Control
Cache-Control: max-age=3600
- max-age:缓存有效期(秒)
- 3600秒 = 1小时
(2)使用Expires
Expires: Wed, 21 Oct 2025 07:28:00 GMT
- 指定过期时间点
2. OkHttp自动处理
// 服务器返回
Response response = client.newCall(request).execute();
// 响应头包含:Cache-Control: max-age=3600
// OkHttp自动:
// 1. 解析Cache-Control
// 2. 计算过期时间
// 3. 保存到缓存
// 4. 下次请求时检查并返回缓存
3. 自定义强制缓存
public class ForceCacheInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Response response = chain.proceed(chain.request());
return response.newBuilder()
.header("Cache-Control", "max-age=3600")
.removeHeader("Pragma")
.build();
}
}
4. 缓存指令
(1)max-age
Cache-Control: max-age=3600
- 缓存3600秒(1小时)
(2)no-cache
Cache-Control: no-cache
- 每次都需要验证
- 不使用强制缓存
(3)no-store
Cache-Control: no-store
- 不缓存
- 每次都请求网络
(4)public/private
Cache-Control: public, max-age=3600
- public:可以被任何缓存缓存
- private:只能被客户端缓存
注意事项:
- 服务器必须设置正确的缓存头
- 注意缓存有效期
- 合理设置缓存时间
8.5 如何实现协商缓存?
答案:
协商缓存通过Last-Modified和ETag实现,OkHttp自动处理。
1. 服务器设置协商缓存
(1)使用Last-Modified
Last-Modified: Wed, 21 Oct 2024 10:00:00 GMT
(2)使用ETag
ETag: "abc123"
2. OkHttp自动处理
// 首次请求
Response response1 = client.newCall(request).execute();
// 服务器返回:Last-Modified: Wed, 21 Oct 2024 10:00:00 GMT
// OkHttp自动保存
// 再次请求
Response response2 = client.newCall(request).execute();
// OkHttp自动添加:If-Modified-Since: Wed, 21 Oct 2024 10:00:00 GMT
// 服务器返回:304 Not Modified
// OkHttp自动使用缓存
3. 工作流程
首次请求:
客户端 → 请求
服务器 ← 200 OK
Last-Modified: Wed, 21 Oct 2024 10:00:00 GMT
ETag: "abc123"
[资源内容]
客户端:保存缓存
再次请求:
客户端 → 请求
If-Modified-Since: Wed, 21 Oct 2024 10:00:00 GMT
If-None-Match: "abc123"
服务器判断:
- 资源未修改 → 304 Not Modified
- 资源已修改 → 200 OK + 新资源
4. 检查协商缓存
Response response = client.newCall(request).execute();
if (response.code() == 304) {
Response cachedResponse = response.cacheResponse();
if (cachedResponse != null) {
String body = cachedResponse.body().string();
}
} else if (response.code() == 200) {
String body = response.body().string();
}
5. ETag vs Last-Modified
| 特性 | ETag | Last-Modified |
|---|---|---|
| 精确度 | 高(内容哈希) | 中(时间戳) |
| 性能 | 需要计算哈希 | 只需读取时间 |
| 适用场景 | 内容经常变化 | 内容变化不频繁 |
| 支持范围 | 任意资源 | 有时间戳的资源 |
注意事项:
- 服务器必须支持协商缓存
- ETag更精确但性能稍差
- Last-Modified简单但精度较低
8.6 如何清除缓存?
答案:
通过Cache对象的方法清除缓存。
1. 清除所有缓存
Cache cache = client.cache();
if (cache != null) {
cache.evictAll();
}
2. 清除特定URL的缓存
public class CacheControlInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
if (request.url().queryParameter("no-cache") != null) {
request = request.newBuilder()
.cacheControl(CacheControl.FORCE_NETWORK)
.build();
}
return chain.proceed(request);
}
}
3. 删除缓存目录
public void clearCacheDirectory(Context context) {
File cacheDir = new File(context.getCacheDir(), "http-cache");
if (cacheDir.exists() && cacheDir.isDirectory()) {
deleteDir(cacheDir);
}
}
4. 条件清除
Cache cache = client.cache();
if (cache != null && cache.size() > 50 * 1024 * 1024) {
cache.evictAll();
}
注意事项:
- 清除缓存会影响性能
- 谨慎清除,避免频繁清除
- 可以部分清除而不是全部清除
8.7 缓存的存储位置在哪里?
答案:
缓存存储在指定的目录中,通常是应用的缓存目录。
1. Android中的存储位置
File cacheDir = context.getCacheDir();
File httpCacheDir = new File(cacheDir, "http-cache");
Cache cache = new Cache(httpCacheDir, 10 * 1024 * 1024);
2. 自定义存储位置
File externalCacheDir = context.getExternalCacheDir();
File httpCacheDir = new File(externalCacheDir, "http-cache");
Cache cache = new Cache(httpCacheDir, 10 * 1024 * 1024);
3. 查看缓存文件
File cacheDir = new File(context.getCacheDir(), "http-cache");
if (cacheDir.exists()) {
File[] files = cacheDir.listFiles();
for (File file : files) {
Log.d("OkHttp", "缓存文件: " + file.getName());
}
}
4. 缓存文件结构
http-cache/
├── 0 (URL哈希值)
├── 1
├── 2
├── ...
└── journal (日志文件,记录缓存操作)
注意事项:
- 使用应用缓存目录,系统可以自动清理
- 确保目录有写权限
- 注意缓存大小限制
8.8 如何自定义缓存策略?
答案:
通过拦截器或自定义Cache实现自定义缓存策略。
1. 条件缓存拦截器
public class ConditionalCacheInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
if (!request.url().toString().contains("/api/")) {
request = request.newBuilder()
.cacheControl(CacheControl.FORCE_NETWORK)
.build();
}
return chain.proceed(request);
}
}
2. 自定义缓存时间
public class CustomCacheInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Response response = chain.proceed(chain.request());
String url = response.request().url().toString();
int maxAge = url.contains("/static/") ? 86400 :
url.contains("/api/") ? 300 : 3600;
return response.newBuilder()
.header("Cache-Control", "max-age=" + maxAge)
.build();
}
}
3. 离线缓存策略
public class OfflineCacheInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
try {
return chain.proceed(chain.request());
} catch (IOException e) {
CacheControl cacheControl = new CacheControl.Builder()
.onlyIfCached()
.maxStale(7, TimeUnit.DAYS)
.build();
Request offlineRequest = chain.request().newBuilder()
.cacheControl(cacheControl)
.build();
return chain.proceed(offlineRequest);
}
}
}
4. 使用CacheControl
CacheControl custom = new CacheControl.Builder()
.maxAge(3600, TimeUnit.SECONDS)
.maxStale(7, TimeUnit.DAYS)
.build();
Request request = new Request.Builder()
.url("https://api.example.com/data")
.cacheControl(custom)
.build();
注意事项:
- 遵循HTTP缓存标准
- 注意缓存大小限制
- 处理缓存失效
- 考虑用户体验