【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 网络开发效率与稳定性。

相关推荐
2501_933923253 分钟前
Spring Bean作用域揭秘:单例、原型、请求与会话的区别
java·后端·spring·java-ee
学编程就要猛21 分钟前
基于Spring AI 的智能聊天机器人
java·spring ai·chat robot
终端安全笔记22 分钟前
描述文件、企业证书、MDM 不是三个名字,是三层
android·ios·智能手机
修炼室23 分钟前
Java开发工程师笔试经验贴【高频知识】
java
木井巳36 分钟前
【记忆化搜索】最长递增子序列
java·算法·leetcode·深度优先·剪枝·推荐算法
杨杨杨大侠43 分钟前
知识库已经有了,Java 程序员还要做什么?Spring AI RAG 实战
java·openai·ai编程
伊信1 小时前
Spring AI 基础学习与应用
java·后端
字节暗面1 小时前
一次游戏安全SO静态分析记录:腾讯ACE与FairGuard加固强度对比
android·逆向工程·游戏加固
夜郎king1 小时前
基于 Java + Playwright 实现网站自动访问与数据采集(以腾讯云开发者社区为例)
java·playwright·网页数据获取
Anastasiozzzz1 小时前
重新定义 Agent 基建:Redis 在现代 AI 与智能体系统中的工程实践
java·人工智能·redis·ai