【Android】OkHttp的使用及封装

【Android】OkHttp的使用及封装

文章目录

  • 【Android】OkHttp的使用及封装
  • [1. OkHttp概述](#1. OkHttp概述)
  • [2. OkHttp使用基本步骤](#2. OkHttp使用基本步骤)
  • [3. 异步GET请求](#3. 异步GET请求)
  • [4. 异步POST请求](#4. 异步POST请求)
    • [4.1 纯文本](#4.1 纯文本)
    • [4.2 JSON数据](#4.2 JSON数据)
    • [4.3 表单数据(x-www-form-urlencoded)](#4.3 表单数据(x-www-form-urlencoded))
    • [4.4 多部分表单(multipart/form-data)------文件上传](#4.4 多部分表单(multipart/form-data)——文件上传)
    • [4.5 自定义二进制流(大文件)](#4.5 自定义二进制流(大文件))
    • 添加请求头
  • [5. 异步下载文件](#5. 异步下载文件)
  • [6. 取消请求](#6. 取消请求)
  • [7. 关于OkHttp的简单封装](#7. 关于OkHttp的简单封装)
  • [8. 总结](#8. 总结)

1. OkHttp概述

OkHttp 是一款由 Square 公司开发的高性能 HTTP 客户端库,广泛应用于 Java 和 Android 平台。它简化了网络请求的发送与响应处理,支持 HTTP/2、连接池、GZIP 压缩和响应缓存等现代网络特性,能显著提升通信效率并降低资源消耗。OkHttp 提供同步和异步两种调用方式,并通过拦截器机制灵活实现日志记录、身份认证、请求重试等通用逻辑。因其稳定、高效且易于扩展,OkHttp 已成为 Android 官方推荐的网络库,也是 Retrofit 等主流框架的底层依赖。

2. OkHttp使用基本步骤

  1. 在build.gradle中添加OkHttp依赖:

    groovy 复制代码
    dependencies {
        implementation("com.squareup.okhttp3:okhttp:4.12.0")
        implementation("com.squareup.okio:okio:3.9.1")
    }
  2. 创建 OkHttpClient 实例(可复用)

    java 复制代码
    OkHttpClient client = new OkHttpClient();
  3. 构建 Request 对象

    java 复制代码
    Request request = new Request.Builder()
        .url("https://api.example.com/data")
        .build();
  4. 发送同步或异步请求

    • 同步请求(须在子线程中执行)

      java 复制代码
      new Thread(() -> {
          try {
              Response response = client.newCall(request).execute();
              if (response.isSuccessful()) {
                  String responseBody = response.body().string();
                  // 在主线程更新 UI(使用 Handler 或 runOnUiThread)
              }
          } catch (IOException e) {
              e.printStackTrace();
          }
      }).start();
    • 异步请求(自动在后台线程中执行)

      java 复制代码
      client.newCall(request).enqueue(new Callback() {
          @Override
          public void onFailure(Call call, IOException e) {
              // 请求失败(网络错误等)
              e.printStackTrace();
          }
      
          @Override
          public void onResponse(Call call, Response response) throws IOException {
              if (response.isSuccessful()) {
                  String responseBody = response.body().string();
                  // 注意:此处仍在子线程,不能直接操作 UI
                  // 需切换到主线程更新 UI
                  runOnUiThread(() -> {
                      // 更新 UI,例如 TextView.setText(responseBody);
                  });
              } else {
                  // 处理 HTTP 错误状态码
              }
          }
      });

3. 异步GET请求

一个简单的GET请求:

java 复制代码
OkHttpClient mOkHttpClient = new OkHttpClient();
public void OkHttpGet() {
        // OkHttpGet 请求
    	// 创建Request
        Request request = new Request.Builder()
                .url("https://jsonplaceholder.typicode.com/posts/1")
                .method("GET", null) // 可以简化成.get(),也可以不写,默认就是GET
                .build();
    	// 异步请求
        mOkHttpClient.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(@NonNull Call call, @NonNull IOException e) {
                Log.e(TAG, "请求失败:" + e.getMessage(), e);
            }

            @Override
            public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
                if (response.isSuccessful()) {
                    String str = response.body().string();
                    Log.d(TAG, "onResponse: 响应长度=" + str.length());
                    Log.d(TAG, "onResponse: 内容=" + str);
                    // 若要更新UI:
                    // runOnUiThread(() -> {
                    //     TextView text = (TextView) findViewById(R.id.text);
                    //     text.setText(str);
                    // });
                } else {
                    Log.e(TAG, "请求失败,状态码:" + response.code());
                }
            }
        });
    }

分析:

  • 使用Request.Builder()构建请求,.url()设置请求地址,.method("GET", null) 显式指定为 GET 请求,进一步可以简化为.get()(不写也可以),因为GET请求不需要请求体。

  • 使用enqueue()发起异步请求,不会阻塞主线程,设置的回调将会在子线程中执行(onResponseonFailure 都不在主线程)。

  • 成功回调中,response.body().string() 只能调用一次,因为底层流会被消费掉,如果后续还需要使用响应内容,应先保存至变量。

  • 如果在响应成功后更新UI,则需切回到主线程,使用runOnUiThread或Handler。

  • 如果想要调用同步GET请求,则可以调用Call的execute方法

4. 异步POST请求

POST请求就是向服务器发送数据,发送数据的格式有很多种,比如纯文本、JSON、文件(multipart)等等。无论发送什么数据,POST请求的基本格式如下:

java 复制代码
OkHttpClient client = new OkHttpClient();

Request request = new Request.Builder()
    .url("https://httpbin.org/post")
    .post(requestBody) // 替换为不同格式的 RequestBody
    .build();

client.newCall(request).enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {
        e.printStackTrace();
    }

    @Override
    public void onResponse(Call call, Response response) throws IOException {
        try (response) { // 自动关闭
            if (response.isSuccessful()) {
                Log.d(TAG, "请求成功:" + response.body().string());
            } else {
                Log.d(TAG, "请求错误,状态码:" + response.code());
            }
        }
    }
});

和GET方法相比,在构造Request时要多发送一个requestBody,对于不同格式的数据,重点在于如何构造不同的

requestBody

4.1 纯文本

适用于发送简单的字符串,如日志、消息、命令等。

java 复制代码
MediaType TEXT_PLAIN = MediaType.get("text/plain; charset=utf-8");
String content = "Hello, OkHttp!";
RequestBody requestBody = RequestBody.create(content, TEXT_PLAIN);

分析:

  • MediaType是OkHttp中用于表示HTTP内容类型(Content-Type)的类,MediaType.get(...) 是 OkHttp 提供的静态工厂方法,用于解析字符串为 MediaType 对象。

  • 这里通过MediaType.get()方法创建了一个表示纯文本,且字符编码为UTF-8的媒体类型,等价于HTTP头部的:

    复制代码
    Content-Type: text/plain; charset=utf-8
  • 使用RequestBody.create()方法,将字符串content和之前定义的MediaType封装成一个RequestBody对象,这个RequestBody对象可以被用于POST、PUT等需要携带请求体的HTTP请求中。RequestBody内部会将字符串按UTF-8编码转换成字节(因为MediaType指定了charset=utf-8

4.2 JSON数据

java 复制代码
MediaType JSON = MediaType.get("application/json; charset=utf-8");
String json = """
    {
      "name": "张三",
      "age": 28,
      "email": "zhangsan@example.com"
    }
    """;
RequestBody requestBody = RequestBody.create(json, JSON);

分析:

  • MediaType.get()创建了一个表示JSON格式、UTF-8编码的媒体类型

  • 使用 RequestBody.create(content, mediaType) 方法创建一个请求体。

  • 将上面的 JSON 字符串和 MediaType 传入,生成一个可用于 POST/PUT 等请求的 RequestBody 对象。

  • 在 OkHttp 的 Request.Builder().post(requestBody) 中可以直接使用。

此外,可以使用Gson生成JSON(Gson可以将Java对象转化为JSON字符串),简化代码。首先添加依赖:

groovy 复制代码
dependencies {
    implementation 'com.google.code.gson:gson:2.10.1'
}

比如有一个Java类:

java 复制代码
// User.java
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将用户对象转化为JSON并通过OkHttp发送:

java 复制代码
User user = new User("张三", 28, "zhangsan@example.com");
// 使用 Gson 序列化
String json = new Gson().toJson(user);
// 创建请求体
MediaType JSON = MediaType.get("application/json; charset=utf-8");
RequestBody body = RequestBody.create(json, JSON);

4.3 表单数据(x-www-form-urlencoded)

模拟 HTML <form> 提交,键值对形式。

java 复制代码
RequestBody requestBody = new FormBody.Builder()
    .add("username", "alice")
    .add("password", "secret123")
    .addEncoded("redirect", "%2Fhome") // 已编码的值
    .build();

分析:

  • .add("username", "alice"):向向表单中添加一个键值对:username=alice,需要注意的是,add(key, value) 方法会自动对 key 和 value 进行 URL 编码 (例如空格变成 %20/ 变成 %2F 等)。

  • .addEncoded("redirect", "%2Fhome"):使用 addEncoded 方法表示:已经手动对值进行了 URL 编码,OkHttp 不应再次编码。这里传入的值是 "%2Fhome",其中 %2F/ 的 URL 编码形式。如果用普通的 .add("redirect", "/home"),OkHttp 会自动将其编码为 %2Fhome。但如果已经编码好了(比如从其他地方拿到的已编码字符串),就用 addEncoded 避免双重编码(否则 %2F 会被再编码成 %252F,这是错误的)。

  • 最终生成的请求体内容:

    复制代码
    username=alice&password=secret123&redirect=%2Fhome

    对应的HTTP头部会包含:

    复制代码
    Content-Type: application/x-www-form-urlencoded

4.4 多部分表单(multipart/form-data)------文件上传

用于同时上传文件和其他字段(如用户ID+头像)

java 复制代码
File imageFile = new File("/path/to/avatar.jpg");

// 根据文件扩展名动态判断MIME类型
String mimeType = URLConnection.guessContentTypeFromName(imageFile.getName());
MediaType mediaType = MediaType.parse(mimeType != null ? mimeType : "application/octet-stream");
// 构造文件部分
RequestBody fileBody = RequestBody.create(imageFile, mimeType);

// 构造 multipart 请求体
MultipartBody requestBody = new MultipartBody.Builder()
    .setType(MultipartBody.FORM)
    .addFormDataPart("userId", "12345")                     // 普通字段
    .addFormDataPart("description", "My profile picture")   // 普通字段
    .addFormDataPart("avatar", "avatar.jpg", fileBody)      // 文件字段(带文件名)
    .build();

分析:

  • 通过URLConnection.guessContentTypeFromName()动态判断MIME类型,该方法会根据文件扩展名来判断。

  • 使用 RequestBody.create(File, MediaType) 方法将文件包装成 OkHttp 的请求体。

  • .setType(MultipartBody.FORM),设置multipart类型为from-data,这是HTML表单上传的标准格式。

  • .addFormDataPart("userId", "12345") :添加一个普通文本字段,字段名为userId,值为12345

  • .addFormDataPart("avatar", "avatar.jpg", fileBody):添加一个文件字段,字段名为"avatar",文件名(在HTTP请求中显示的名称)为"avatar.jpg", 内容就是前面创建的fileBody

    在HTTP中,大致是以下内容:

    复制代码
    Content-Disposition: form-data; name="avatar"; filename="avatar.jpg"
    Content-Type: image/jpeg
    
    文件二进制数据

4.5 自定义二进制流(大文件)

避免将大文件全部加载到内存,使用流式写入。

java 复制代码
RequestBody requestBody = new RequestBody() {
    @Override
    public MediaType contentType() {
        return MediaType.get("application/octet-stream");
    }

    @Override
    public void writeTo(BufferedSink sink) throws IOException {
        // try-with-resources打开文件的输入流,自动管理流,防止资源泄露
        try (InputStream in = new FileInputStream("huge-file.bin")) {
            // 将InputStream包装为Okio的Source
            Source source = Okio.source(in);
            sink.writeAll(source); // 流式写入,不占大量内存
        }
    }
};

分析:

  • contentType()方法告诉服务器这个请求的MIME类型,MediaType.get("application/octet-stream");表示这是一个二进制流数据,通常用于上传任意类型的二进制文件(如图片、视频、压缩包等)。

  • writTo(BufferedSink sink)方法:当OkHttp准备发送HTTP请求时,它会调用此方法,将请求写入到网络连接中去,方法参数BufferedSink是Okio(OkHttp内部使用的I/O库)提供的高效字节输出流接口

  • Okio.source(InputStream) 是 Okio 提供的工具方法,将标准Java的InputStream转换为Okio的Source

  • sink.writeAll(source):流式写入,它会持续地从source读取数据,并写入到sink,直到文件结束。这个过程是分块进行的(内部使用缓冲区,比如8KB或64KB),不会一次性把整个文件加载到内存。因此即使文件是几个GB,也只占用少量内存(仅缓冲区大小),非常适合上传大文件。

  • 最后使用try-with-resources语法打开的流,无论是否发生异常,流都会被自动关闭,防止资源泄露。

添加请求头

无论哪种格式,都可以添加自定义Headers:

java 复制代码
Request request = new Request.Builder()
    .url(url)
    .post(requestBody)
    .header("Authorization", "Bearer xxx")
    .header("User-Agent", "MyApp/1.0")
    .build();

分析:

  • Authentication:身份认证,这是API最常用的认证方式之一,Bearer表示"持有该令牌(token)的人即被授权",xxx是从登录接口获取的访问令牌(Access Token)。服务器接收到后,会检查整个token是否有效、是否过期、是否有权限访问该接口,如果缺失或无效,通常会返回401 Unauthorized

  • User-Agent:告诉服务器这个请求是从哪个应用/浏览器/设备发来的。

5. 异步下载文件

下载文件和之前的发送数据不同,要下载一张图片,在得到Response后将流写入到我们指定的图片文件中,代码如下:

java 复制代码
public void downloadFile() {
        String url = "https://httpbin.org/image/jpeg";
        Request request = new Request.Builder().url(url).build();
        mOkHttpClient.newCall(request).enqueue(new Callback() {

            @Override
            public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
                if (!response.isSuccessful()) {
                    Log.e(TAG, "图片下载失败,状态码: " + response.code());
                    response.close();
                    return;
                }

                // 获取应用私有外部存储目录(用于图片),若不可用则回退到内部存储
                File dir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);
                if (dir == null) {
                    dir = getFilesDir(); // 内部私有目录
                }

                String filename = "image_" + System.currentTimeMillis() + ".jpg";
                File file = new File(dir, filename);

                FileOutputStream fos = null;
                InputStream is = null;
                try {
                    is = response.body().byteStream();
                    fos = new FileOutputStream(file);

                    // 使用缓存区提升I/O性能
                    byte[] buffer = new byte[2048]; // 2KB缓冲区
                    int len;
                    while ((len = is.read(buffer)) != -1) {
                        fos.write(buffer, 0, len);
                    }
                    // 强制将FileOutputStream缓冲区的数据立即写入磁盘
                    fos.flush();

                    Log.d(TAG, "图片成功下载至:" + file.getAbsolutePath());
                    runOnUiThread(() -> {
                        Toast.makeText(MainActivity.this, "图片下载成功!", Toast.LENGTH_SHORT).show();
                    });

                } catch (IOException e) {
                    Log.e(TAG, "图片下载失败!", e);
                } finally {
                    try {
                        if (fos != null) fos.close();
                        if (is != null) is.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    response.close(); // 显式释放资源
                }
            }

            @Override
            public void onFailure(@NonNull Call call, @NonNull IOException e) {

            }
        });
    }

分析:

  • File dir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);使用应用私有目录,仅当前应用可直接访问,其他应用无法读写,无需动态申请权限,同时应用卸载时会自动清理。

  • "image_" + System.currentTimeMillis() + ".jpg";使用唯一文件名避免覆盖,这样每次下载都会生成新文件,防止旧图被覆盖。

  • is = response.body().byteStream();获取响应体的原始字节输入流,使用byteStream()流式读取,适合大文件,避免内存溢出。相比response.body().bytes()(一次性加载全部字节到内存)。需要注意的是,该流必须手动关闭,或者确保被完全读取。

  • fos = new FileOutputStream(file);创建一个指向目标文件file的字节输出流,如果文件不存在,会自动创建(前提是目录存在且有读写权限)。如果文件已存在,默认会覆盖原内容。

6. 取消请求

使用call.cancel()可以立即停止一个正在实行的call。当用户离开一个应用时或者跳转到其他界面时,使用call.cancel()可以节约网络资源;另外,不管同步还是异步的call都可以取消,也可以通过tag来同时取消多个请求。当构建一个请求时,使用Request.Builder.tag(Object tag)来分配一个标签,之后就可以用OkHttpClient.cancel(Object tag)来取消所有带有这个tag的call,具体代码如下:

java 复制代码
private ScheduledExecutorService executor = new ScheduledThreadPoolExecutor(1);
    public void cancel() {
        final Request request = new Request.Builder()
                .url("http://www.baidu.com")
                .cacheControl(CacheControl.FORCE_NETWORK) // 1
                .build();
        Call call = null;
        call = mOkHttpClient.newCall(request);
        final Call finalCall = call;
        // 1ms后取消call
        executor.schedule(new Runnable() {
            @Override
            public void run() {
                finalCall.cancel();
            }
        }, 1, TimeUnit.MICROSECONDS);
        call.enqueue(new Callback() {
            @Override
            public void onFailure(@NonNull Call call, @NonNull IOException e) {

            }

            @Override
            public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
                if (response.cacheResponse() != null) {
                    String str = response.cacheResponse().toString();
                    Log.d(TAG, "cache---" + str);
                } else {
                    String str = response.networkResponse().toString();
                    Log.d(TAG, "network---" + str);
                }
            }
        });
    }

创建定时线程池,1ms后调用call.cancel()来取消请求。为了能让请求耗时,在上面代码注释1处设置每次请求都要请求网络,运行程序并不断调用cancel方法,这样应该没有Log打印出来,因为每个请求都被取消了。

7. 关于OkHttp的简单封装

每次请求网络都需要写重复的代码,使用起来非常麻烦。这时就可以对OkHttp进行封装。封装的意义就在于可更加方便的使用,且具有扩展性。对OkHttp进行封装最需要解决的是一下两点:

  1. 避免重复代码调用;
  2. 将请求结果回调到UI线程。

基于以上两点,这里也对OkHttp简单封装一下。

首先写一个抽象类用于请求回调:

java 复制代码
public abstract class ResultCallback {
    public abstract void onError(Request request, Exception e);
    public abstract void onResponse(String str) throws IOException;
}

接下来封装OkHttp,这里只实现了异步GET请求,如下所示:

java 复制代码
package com.example.okhttpdemo;

import android.content.Context;
import android.os.Handler;

import androidx.annotation.NonNull;

import java.io.File;
import java.io.IOException;
import java.util.concurrent.TimeUnit;

import okhttp3.Cache;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

public class OkHttpEngine {
    private static volatile OkHttpEngine mInstance;
    private OkHttpClient mOkHttpClient;
    private Handler mHandler;

    public static OkHttpEngine getInstance(Context context) {
        if (mInstance == null) {
            synchronized (OkHttpEngine.class) {
                if (mInstance == null) {
                    mInstance = new OkHttpEngine(context);
                }
            }
        }
        return mInstance;
    }

    private OkHttpEngine(Context context) {
        File sdcache = context.getExternalCacheDir();
        int cacheSize = 10 * 1024 * 1024;
        OkHttpClient.Builder builder = new OkHttpClient.Builder()
                .connectTimeout(15, TimeUnit.SECONDS)
                .writeTimeout(20, TimeUnit.SECONDS)
                .readTimeout(20, TimeUnit.SECONDS)
                .cache(new Cache(sdcache.getAbsoluteFile(), cacheSize));
        mOkHttpClient = builder.build();
        mHandler = new Handler();
    }

    /**
     * 异步GET请求
     * @param url
     * @param callback
     */

    public void getAsynHttp(String url, ResultCallback callback) {
        final Request request = new Request.Builder()
                .url(url)
                .build();
        Call call = mOkHttpClient.newCall(request);
        dealResult(call, callback);
    }

    private void dealResult(Call call, final ResultCallback callback) {
        call.enqueue(new Callback() {
            @Override
            public void onFailure(@NonNull Call call, @NonNull IOException e) {
                sendFailedCallback(call.request(), e, callback);
            }

            @Override
            public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
                sendFailedCallback(response.body().string(), callback);
            }

            private void sendFailedCallback(final String str, final ResultCallback  callback) {
                mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        if (callback != null) {
                            try {
                                callback.onResponse(str);
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                });
            }
            private void sendFailedCallback(final Request request, final Exception e, final ResultCallback callback) {
                mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        if (callback != null) {
                            callback.onError(request, e);
                        }
                    }
                });
            }
        });

    }
}

其原理就是写一个双重检查模式的单例,在开始创建的时候配置好OkHttpClient,并创建Handler,在请求网络的时候用Handler将请求的结果回调给UI线程。当想要请求网络时就调用OkHttpEngine的getAsynHttp方法,如下所示:

java 复制代码
OkHttpEngine.getInstance(MainActivity.this).getAsynHttp("https://jsonplaceholder.typicode.com/posts/1", new ResultCallback() {
    @Override
    public void onError(Request request, Exception e) {

    }

    @Override
    public void onResponse(String str) throws IOException {
        Log.d(TAG, str);
        Toast.makeText(getApplicationContext(), "请求成功!", Toast.LENGTH_SHORT).show();
    }
});

8. 总结

  1. OkHttp 是 Android 推荐的高效 HTTP 客户端,支持多种协议与优化特性。
  2. 支持同步与异步请求,常用异步方式避免阻塞主线程。
  3. 可发送多种类型的 POST 请求(JSON、表单、文件等)。
  4. 提供统一回调封装、自动切换主线程、请求取消、文件下载等实用功能。
  5. 通过单例和工具类可实现简洁、可复用的网络请求封装。

总结:OkHttp 功能强大、使用灵活,配合合理封装能显著提升 Android 网络开发效率与稳定性。

相关推荐
黄林晴2 小时前
重启不用输 PIN!Android 17 终于把 SIM 卡安全做明白了
android
CHANG_THE_WORLD2 小时前
PDF结构的清晰图示
java·服务器·pdf
MinterFusion2 小时前
Java后端高频术语表
java·开发语言·后端·程序员·大厂面试·术语
indexsunny2 小时前
互联网大厂Java面试实录:Spring Boot到微服务的深入探讨
java·spring boot·微服务·面试·eureka·kafka·jwt
鸽鸽程序猿2 小时前
【JavaEE】【SpringAI】Tool Calling(工具调用)
java·java-ee
于先生吖2 小时前
高并发稳定运营,JAVA 动漫短剧小程序 + H5 源码
java·开发语言·小程序
2501_915921432 小时前
uni-app一键生成iOS安装包并上传TestFlight全流程
android·ios·小程序·https·uni-app·iphone·webview
云和数据.ChenGuang2 小时前
鸿蒙应用对接DeepSeek大模型:构建智能问答系统的技术实践
java·华为·langchain·harmonyos·euler·openduler
曹牧2 小时前
在 Eclipse 中变更 SVN 地址
java·svn·eclipse