OkHttp & Retrofit 面试题答案——上

3 阅读1小时+

第一部分:OkHttp 基础

第一章:OkHttp 概述(7 题)

1.1 OkHttp是什么?它的作用是什么?

答案:

OkHttp是一个开源的HTTP客户端库,由Square公司开发,用于Android和Java应用程序中发送HTTP请求和处理HTTP响应。

OkHttp的定义:

  • 一个HTTP/HTTP2客户端
  • 支持同步和异步请求
  • 提供连接池、缓存、拦截器等高级特性
  • 默认支持GZIP压缩响应缓存

OkHttp的主要作用:

  1. 发送HTTP请求

    • 支持GET、POST、PUT、DELETE等HTTP方法
    • 支持HTTP/1.1和HTTP/2.0协议
  2. 处理HTTP响应

    • 自动处理响应头
    • 支持流式响应处理
    • 自动解压GZIP响应
  3. 网络优化

    • 连接池复用,减少连接开销
    • 自动重试失败的请求
    • 支持请求和响应缓存
  4. 安全性

    • 支持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等框架的底层实现
    • 社区活跃,文档完善
    • 持续更新维护

与其他框架对比:

特性OkHttpHttpURLConnection
连接池
HTTP/2.0
拦截器
缓存手动
易用性

1.3 OkHttp和HttpURLConnection的区别是什么?

答案:

OkHttp和HttpURLConnection是Android中两种主要的HTTP客户端,它们的主要区别如下:

对比项OkHttpHttpURLConnection
架构设计基于拦截器链,模块化架构,易于扩展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支持

主要里程碑:

  1. 2012年:OkHttp首次发布
  2. 2014年:引入拦截器机制
  3. 2016年:OkHttp 3.x发布,API重大改进
  4. 2019年:OkHttp 4.x发布,Kotlin支持
  5. 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
  • 包含连接池、缓存等功能

可选依赖:

  1. 日志拦截器

    implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0'
    
    • 用于调试和日志记录
    • 打印请求和响应信息
  2. 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.1HTTP/2.0HTTP/3
多路复用
头部压缩
连接建立TCP握手TCP握手QUIC(更快)
传输层TCPTCPUDP
移动网络一般优秀

实际使用:

  • 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 DataJSON
Content-Typeapplication/x-www-form-urlencodedapplication/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-catchonFailure回调
取消请求较困难容易(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. 注意事项

  • 回调在后台线程onResponseonFailure都在后台线程执行,更新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的onFailureonResponse方法中进行。

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下载文件的核心是将响应体的字节流写入本地文件。有两种方式:流式下载一次性读取,根据文件大小选择合适的方案。

下载流程说明:

  1. 发送GET请求获取文件资源
  2. 从ResponseBody获取输入流
  3. 创建本地文件输出流
  4. 将数据从输入流复制到输出流
  5. 关闭所有流资源

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的核心机制,用于在请求发送前和响应返回后进行处理。

拦截器的定义:

  • 一个接口,实现请求和响应的拦截处理
  • 可以修改请求修改响应重试请求
  • 通过责任链模式串联多个拦截器

拦截器的作用:

  1. 请求处理

    • 添加公共请求头
    • 添加认证信息
    • 修改请求参数
    • 记录请求日志
  2. 响应处理

    • 解析响应数据
    • 统一错误处理
    • 响应缓存
    • 响应解密
  3. 功能扩展

    • 请求重试
    • 请求加密
    • 性能监控
    • 数据统计

拦截器接口:

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 InterceptorNetwork 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连接,减少连接建立的开销。

连接池的作用:

  1. 连接复用

    • 复用已建立的TCP连接
    • 避免频繁建立和关闭连接
    • 减少TCP握手开销
  2. 性能提升

    • 减少连接建立时间
    • 提高请求响应速度
    • 降低网络延迟
  3. 资源管理

    • 管理连接的生命周期
    • 控制连接数量
    • 自动清理空闲连接
  4. 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

特性ETagLast-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缓存标准
  • 注意缓存大小限制
  • 处理缓存失效
  • 考虑用户体验