深入理解 OkHttp3:从基本使用到架构设计的全面指南

905 阅读6分钟

一、OkHttp3:Android 网络请求的事实标准

OkHttp 是 Square 公司开发的高性能网络请求框架,在 Android 开发中几乎是必选方案。其核心优势如同 "智能快递系统":

  • HTTP/2 支持:像多车道高速公路,同一主机的所有请求共享连接,减少握手开销

  • 连接池:类似快递分拣中心,复用连接避免重复建立(默认保持 5 分钟)

  • GZIP 压缩:数据打包前压缩,减少传输体积(类似快递装箱前折叠物品)

  • 智能重试:请求失败时自动尝试其他 IP,如同快递地址错误时自动查找备用地址

数据对比:相比 Java 原生 HttpURLConnection,OkHttp 在典型场景下可减少 30% 的网络延迟

二、快速入门:从依赖到第一个请求

2.1 环境准备

groovy

// 引入核心依赖(3.14.x为Java版本,4.0+为Kotlin)
implementation 'com.squareup.okhttp3:okhttp:3.14.7'
// 底层数据处理库
implementation 'com.squareup.okio:okio:1.17.5'

xml

<!-- 必须申请的权限 -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- 如需缓存,添加读写权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
2.2 GET 请求:获取百度首页

java

OkHttpClient client = new OkHttpClient(); // 创建客户端

Request request = new Request.Builder()
    .url("https://www.baidu.com")
    .get() // 明确声明GET方法(可省略,默认即为GET)
    .build();

// 同步请求(必须在子线程执行)
new Thread(() -> {
    try {
        Response response = client.newCall(request).execute();
        Log.d("OkHttp", "响应内容:" + response.body().string());
    } catch (IOException e) {
        e.printStackTrace();
    }
}).start();

// 异步请求(推荐方式,自动处理线程)
client.newCall(request).enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {
        Log.e("OkHttp", "请求失败", e);
    }
    @Override
    public void onResponse(Call call, Response response) throws IOException {
        // 注意:回调在子线程,更新UI需切换主线程
        String html = response.body().string();
        runOnUiThread(() -> textView.setText(html));
    }
});

核心要点

  • 每个Call只能执行一次,重复调用会抛出异常
  • 同步请求execute()会阻塞线程,必须在子线程调用
  • 异步请求enqueue()内部维护线程池,回调在子线程

三、POST 请求:从简单文本到复杂文件上传

3.1 提交普通文本 / JSON

java

// 提交Markdown文本
MediaType markdownType = MediaType.parse("text/x-markdown; charset=utf-8");
RequestBody body = RequestBody.create(markdownType, "**Hello World**");

Request request = new Request.Builder()
    .url("https://api.github.com/markdown/raw")
    .post(body)
    .build();

// 提交JSON数据
String json = "{"name":"OkHttp","version":3.14}";
RequestBody jsonBody = RequestBody.create(
    MediaType.parse("application/json; charset=utf-8"), json);
3.2 表单提交(最常用场景)

java

// 构建表单数据
RequestBody formBody = new FormBody.Builder()
    .add("username", "hfy")
    .add("password", "123456")
    .add("remember", "true")
    .build();

Request request = new Request.Builder()
    .url("https://example.com/login")
    .post(formBody)
    .build();
3.3 复杂请求体:同时上传表单和文件

java

// 准备文件RequestBody
File file = new File(Environment.getExternalStorageDirectory(), "avatar.jpg");
RequestBody fileBody = RequestBody.create(
    MediaType.parse("image/jpeg"), file);

// 构建多部分请求体
MultipartBody multipartBody = new MultipartBody.Builder()
    .setType(MultipartBody.FORM) // 声明为表单类型
    .addFormDataPart("username", "hufeiyang")
    .addFormDataPart("phone", "13800138000")
    // 第三个参数是文件RequestBody
    .addFormDataPart("avatar", "profile.jpg", fileBody)
    .build();

Request request = new Request.Builder()
    .url("https://example.com/upload")
    .post(multipartBody)
    .build();

抓包分析

http

POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Length: 123456

----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="username"

hufeiyang
----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="phone"

13800138000
----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="avatar"; filename="profile.jpg"
Content-Type: image/jpeg

[文件二进制数据...]

四、高级配置:从超时到拦截器的全场景优化

4.1 全局客户端配置

java

OkHttpClient client = new OkHttpClient.Builder()
    // 连接超时:建立TCP连接的最大等待时间
    .connectTimeout(15, TimeUnit.SECONDS)
    // 读取超时:接收响应数据的最大等待时间
    .readTimeout(20, TimeUnit.SECONDS)
    // 写入超时:发送请求数据的最大等待时间
    .writeTimeout(10, TimeUnit.SECONDS)
    
    // 缓存配置(500MB缓存空间)
    .cache(new Cache(getExternalCacheDir(), 500 * 1024 * 1024))
    
    // 日志拦截器(关键功能!)
    .addInterceptor(chain -> {
        Request request = chain.request();
        long start = System.currentTimeMillis();
        
        // 记录请求信息
        Log.d("OkHttp", "请求开始: " + request.url());
        Log.d("OkHttp", "请求头: " + request.headers());
        
        // 执行实际请求
        Response response = chain.proceed(request);
        long end = System.currentTimeMillis();
        
        // 记录响应信息
        Log.d("OkHttp", "请求结束: " + response.code());
        Log.d("OkHttp", "耗时: " + (end - start) + "ms");
        
        return response;
    })
    
    // 构建全局唯一的客户端实例
    .build();
4.2 单个请求配置

java

Request request = new Request.Builder()
    .url("https://example.com/data")
    // 添加自定义请求头
    .addHeader("Authorization", "Bearer YOUR_TOKEN")
    .addHeader("Accept", "application/json")
    
    // 缓存策略(覆盖全局设置)
    .cacheControl(CacheControl.FORCE_NETWORK) // 强制走网络
    // .cacheControl(CacheControl.FORCE_CACHE) // 强制走缓存
    
    .get()
    .build();
4.3 缓存策略详解
策略行为描述
FORCE_NETWORK直接请求网络,不读取缓存,响应会写入缓存
FORCE_CACHE只读取缓存,若缓存不存在则报错
CACHE_IF_AVAILABLE先查缓存,若缓存过期(根据响应头max-age)则请求网络
NO_CACHE不使用缓存,每次都请求网络
NO_STORE不使用缓存,且不保存响应到缓存

五、实战技巧:从异常处理到架构整合

5.1 优雅的异常处理

java

client.newCall(request).enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {
        // 网络异常分类处理
        if (e instanceof SocketTimeoutException) {
            showToast("网络超时,请重试");
        } else if (e instanceof ConnectException) {
            showToast("连接失败,请检查网络");
        } else {
            showToast("网络错误: " + e.getMessage());
        }
    }
    
    @Override
    public void onResponse(Call call, Response response) throws IOException {
        if (!response.isSuccessful()) {
            // 服务器错误处理
            showToast("请求失败: " + response.code());
            return;
        }
        
        // 处理成功响应
        String data = response.body().string();
        // 注意:response.body().string()只能调用一次,多次调用会报错
    }
});
5.2 与 MVVM 架构整合

java

public class UserViewModel extends ViewModel {
    private final OkHttpClient client;
    private final MutableLiveData<User> userLiveData = new MutableLiveData<>();
    private final MutableLiveData<String> errorLiveData = new MutableLiveData<>();
    
    public UserViewModel() {
        client = createOkHttpClient(); // 创建配置好的OkHttpClient
    }
    
    public LiveData<User> getUser(String userId) {
        Request request = new Request.Builder()
            .url("https://api.example.com/users/" + userId)
            .build();
            
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onResponse(Call call, Response response) throws IOException {
                if (response.isSuccessful() && response.body() != null) {
                    String json = response.body().string();
                    User user = GsonUtil.fromJson(json, User.class);
                    userLiveData.postValue(user);
                } else {
                    errorLiveData.postValue("获取用户失败: " + response.code());
                }
            }
            
            @Override
            public void onFailure(Call call, IOException e) {
                errorLiveData.postValue("网络错误: " + e.getMessage());
            }
        });
        
        return userLiveData;
    }
}
5.3 大文件下载优化

java

// 断点续传下载(关键在请求头Range)
Request request = new Request.Builder()
    .url("https://example.com/bigfile.apk")
    .addHeader("Range", "bytes=" + downloadedBytes + "-") // 已下载字节数
    .build();
    
Response response = client.newCall(request).execute();
if (response.code() == 206) { // 206表示部分内容
    try (InputStream is = response.body().byteStream();
         FileOutputStream fos = new FileOutputStream(file, true)) { // true表示追加写入
        byte[] buffer = new byte[8192];
        int len;
        while ((len = is.read(buffer)) != -1) {
            fos.write(buffer, 0, len);
        }
    }
}

六、核心原理:OkHttp 的拦截器机制

拦截器是 OkHttp 的 "灵魂",其工作原理如同快递分拣流水线:

  1. 应用拦截器:处理应用层逻辑(如日志、请求头添加)

  2. 网络拦截器:处理网络层逻辑(如缓存、重试)

  3. 重试与重定向拦截器:处理请求失败重试

  4. 桥接拦截器:处理 HTTP 头转换

  5. 缓存拦截器:处理缓存策略

  6. 连接拦截器:管理连接池

java

// 自定义拦截器示例
Interceptor logInterceptor = chain -> {
    Request request = chain.request();
    long start = System.nanoTime();
    
    // 请求前处理(添加公共参数、日志记录)
    Request newRequest = request.newBuilder()
        .addHeader("App-Version", BuildConfig.VERSION_NAME)
        .addHeader("Device-ID", getDeviceId())
        .build();
    
    Response response = chain.proceed(newRequest); // 调用下一个拦截器
    
    // 请求后处理(响应解析、错误处理)
    long end = System.nanoTime();
    long duration = (end - start) / 1000000; // 转换为毫秒
    
    Log.d("OkHttp", "请求耗时: " + duration + "ms");
    Log.d("OkHttp", "响应码: " + response.code());
    
    return response;
};

七、性能优化与陷阱规避

  1. 连接池优化

    java

    // 自定义连接池(保持100个连接,存活时间5分钟)
    .connectionPool(new ConnectionPool(100, 5, TimeUnit.MINUTES))
    
  2. GZIP 压缩

    java

    // 自动压缩请求体(节省30-50%流量)
    .addInterceptor(new HttpLoggingInterceptor().setLevel(Level.BODY))
    
  3. 陷阱规避

    • 响应体必须关闭:response.body().close()(OkHttp3.8 + 已自动处理,但显式关闭更安全)
    • 避免重复读取响应体:response.body().string()只能调用一次
    • 注意内存泄漏:Activity 销毁时取消未完成的请求call.cancel()

八、总结:OkHttp 的设计哲学

OkHttp 的强大源于其 "可扩展的管道架构",通过拦截器机制让开发者可以无缝介入请求的每个环节。在实际开发中,建议:

  1. 创建全局唯一的 OkHttpClient 实例,避免重复创建开销

  2. 用拦截器统一处理公共逻辑(日志、认证、参数添加)

  3. 结合 ViewModel 和 LiveData 实现响应式网络请求

  4. 针对不同场景设置合理的缓存策略

记住:OkHttp 不仅是一个网络请求工具,更是理解现代 Android 架构的重要入口。掌握其核心原理,能帮助你在处理复杂网络场景时更加游刃有余。