【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使用基本步骤
-
在build.gradle中添加OkHttp依赖:
groovydependencies { implementation("com.squareup.okhttp3:okhttp:4.12.0") implementation("com.squareup.okio:okio:3.9.1") } -
创建 OkHttpClient 实例(可复用)
javaOkHttpClient client = new OkHttpClient(); -
构建 Request 对象
javaRequest request = new Request.Builder() .url("https://api.example.com/data") .build(); -
发送同步或异步请求
-
同步请求(须在子线程中执行)
javanew 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(); -
异步请求(自动在后台线程中执行)
javaclient.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()发起异步请求,不会阻塞主线程,设置的回调将会在子线程中执行(onResponse和onFailure都不在主线程)。 -
成功回调中,
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进行封装最需要解决的是一下两点:
- 避免重复代码调用;
- 将请求结果回调到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. 总结
- OkHttp 是 Android 推荐的高效 HTTP 客户端,支持多种协议与优化特性。
- 支持同步与异步请求,常用异步方式避免阻塞主线程。
- 可发送多种类型的 POST 请求(JSON、表单、文件等)。
- 提供统一回调封装、自动切换主线程、请求取消、文件下载等实用功能。
- 通过单例和工具类可实现简洁、可复用的网络请求封装。
总结:OkHttp 功能强大、使用灵活,配合合理封装能显著提升 Android 网络开发效率与稳定性。