Android-网络请求框架

OkHttp

请求器和请求对象

使用OkHttp发送网络请求,最重要的是OkHttpClientRequest这两个类,前者是请求器,后者是请求对象

1
2
3
4
// 创建请求器
OkHttpClient okHttpClient = new OkHttpClient.Builder().build();
// 先创建请求对象的构建器,在确定请求地址和请求方式后,再构建出请求对象
Request.Builder builder = new Request.Builder();

确定请求地址和请求方式后,再构建出请求对象,如下示例构建get请求对象

1
2
3
Request request = builder.url("https://m1.apifoxmock.com/m1/8092681-7849123-default/get/common")
.get()
.build();

发送get请求

通过请求器发送请求对象,得到一个Call对象,得到Call对象后可以选择执行同步请求或异步请求。

1
2
3
4
5
Request request = builder.url("https://m1.apifoxmock.com/m1/8092681-7849123-default/get/common")
.get()
.build();
// 发送请求,得到一个Call对象
Call call = okHttpClient.newCall(request);

同步请求

同步请求会阻塞当前线程,直到请求完成,因此无法在主线程中执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 在子线程中执行同步请求
new Thread(() -> {
try (Response response = call.execute()) {
// 请求成功
if (response.isSuccessful()) {
LogUtil.d(TAG, "请求成功,code=" + response.code());
ResponseBody responseBody = response.body();
if (responseBody != null) {
String result = responseBody.string();
LogUtil.d(TAG, "response: " + result);
} else {
LogUtil.d(TAG, "response=null");
}
} else {
LogUtil.d(TAG, "请求失败,code=" + response.code());
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}).start();

异步请求

异步请求不阻塞当前线程,发送异步请求需要传入一个Callback对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
call.enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
LogUtil.d(TAG, "onFailure: " + e.getMessage());
}

@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
if (response.isSuccessful()) {
LogUtil.d(TAG, "请求成功,code=" + response.code());
ResponseBody responseBody = response.body();
if (responseBody != null) {
String result = responseBody.string();
LogUtil.d(TAG, "onResponse: response=" + result);
} else {
LogUtil.d(TAG, "onResponse: response=null");
}
} else {
LogUtil.d(TAG, "请求失败,code=" + response.code());
}
response.close();
}
});

文件下载

Response对象执行如下操作,即可实现文件下载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
if (response.isSuccessful()) {
LogUtil.d(TAG, "请求成功,code=" + response.code());
ResponseBody responseBody = response.body();
if (responseBody != null) {
long totalBytes = responseBody.contentLength();
File file = new File("/storage/emulated/0/pig.png");
try (InputStream inputStream = responseBody.byteStream();
FileOutputStream outputStream = new FileOutputStream(file)) {
byte[] buffer = new byte[4096];
int len;
long downloaded = 0;
while ((len = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, len);
downloaded += len;

int percent = (int) (downloaded * 100L / totalBytes);
LogUtil.d(TAG, "下载进度: " + percent + "% (" + downloaded + "/" + totalBytes + " bytes)");
}
outputStream.flush();
}
LogUtil.d(TAG, "文件下载成功");
} else {
LogUtil.d(TAG, "onResponse: response=null");
}
} else {
LogUtil.d(TAG, "请求失败,code=" + response.code());
}
response.close();

发送post请求

发送post请求,在构建Request对象时,需要传入一个RequestBody对象,如下示例发送json数据

1
2
3
4
5
6
7
8
// 创建请求体,这里以发送json数据为例
String json = "{ \"email\": \"x@xxin.xyz\", \"password\": \"123456\"}";
RequestBody requestBody = RequestBody.create(json, MediaType.parse("application/json; charset=utf-8"));

Request request = builder.url("https://m1.apifoxmock.com/m1/8092681-7849123-default/post/login")
.post(requestBody)
.build();
Call call = okHttpClient.newCall(request);

发送表单数据

1
2
3
4
5
6
7
8
RequestBody requestBody = new FormBody.Builder()
.add("email", "x@xxin.xyz")
.add("password", "123456")
.build();
Request request = builder.url("https://m1.apifoxmock.com/m1/8092681-7849123-default/post/login")
.post(requestBody)
.build();
Call call = okHttpClient.newCall(request);

同步、异步请求的发送方式与get请求相同,不再赘述

文件上传

单文件上传

1
2
3
4
5
6
7
File file = new File("/storage/emulated/0/test.txt");
RequestBody requestBody = RequestBody.create(file, MediaType.parse("application/octet-stream"));

Request request = builder.url("https://www.httpbin.org/post")
.post(requestBody)
.build();
Call call = okHttpClient.newCall(request);

多文件上传

1
2
3
4
5
6
7
8
9
MultipartBody multipartBody = new MultipartBody.Builder()
.addFormDataPart("file1", file1.getName(), requestBody1)
.addFormDataPart("file2", file2.getName(), requestBody2)
.build();

Request request = builder.url("https://www.httpbin.org/post")
.post(multipartBody)
.build();
Call call = okHttpClient.newCall(request);

带进度的文件上传

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
File file = new File("/storage/emulated/0/test.txt");
RequestBody fileBody = RequestBody.create(file, MediaType.parse("application/octet-stream"));
RequestBody requestBody = new ProgressRequestBody(fileBody, (written, total) -> {
if (total > 0) {
int percent = (int) (written * 100L / total);
LogUtil.d(TAG, "上传进度: " + percent + "% (" + written + "/" + total + " bytes)");
} else if (total == 0) {
LogUtil.d(TAG, "上传进度: 空文件");
} else {
LogUtil.d(TAG, "上传进度: 文件错误");
}
});
Request request = builder.url("https://www.httpbin.org/post")
.post(requestBody)
.build();
Call call = okHttpClient.newCall(request);

重写RequestBody,在写入数据时统计已写入字节数,并通过回调接口通知上传进度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public final class ProgressRequestBody extends RequestBody {
private final RequestBody delegate;
private final ProgressListener progressListener;

public ProgressRequestBody(RequestBody delegate, ProgressListener progressListener) {
this.delegate = delegate;
this.progressListener = progressListener;
}

@Override
public MediaType contentType() {
return delegate.contentType();
}

@Override
public long contentLength() throws IOException {
return delegate.contentLength();
}

@Override
public void writeTo(@NonNull BufferedSink sink) throws IOException {
// 创建一个CountingSink来统计写入字节数
CountingSink countingSink = new CountingSink(sink);
BufferedSink progressSink = Okio.buffer(countingSink);
// 执行真正的写入操作
delegate.writeTo(progressSink);
progressSink.flush();
}

// 内部类,代理Sink以统计写入字节数
private class CountingSink extends ForwardingSink {
private long bytesWritten = 0; // 已上传长度
private long contentLength = -1; // 总长度

public CountingSink(BufferedSink delegate) {
super(delegate);
}

@Override
public void write(@NonNull Buffer source, long byteCount) throws IOException {
super.write(source, byteCount);
if (contentLength == -1) {
contentLength = contentLength(); // 获取总长度
}
bytesWritten += byteCount;
// 触发进度回调
if (progressListener != null) {
progressListener.onProgress(bytesWritten, contentLength);
}
}
}

public interface ProgressListener {
void onProgress(long bytesWritten, long totalBytes);
}
}

拦截器

Okhttp的拦截器采用责任链模式,每个拦截器通过chain.proceed(Request)把请求(Request对象)交给责任链中的下个拦截器处理,chain.proceed(Request)可以得到下个拦截器返回的响应(Response对象),并且需要把响应返回给上个拦截器。

OkHttp拦截器责任链的完整执行顺序如下

责任链示例图

应用拦截器

应用拦截器可以添加多个,先添加的拦截器先处理请求,但最后处理响应。对于日志、统一添加Header等不关心网络中间过程的场景,优先使用应用拦截器,对于需要观察网络重定向、处理响应压缩等场景,考虑使用网络拦截器

如下拦截器,用于在请求发送之前添加统一的请求头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class HeaderInterceptor implements Interceptor {
@NonNull
@Override
public Response intercept(@NonNull Chain chain) throws IOException {
Request originalRequest = chain.request();

// 在请求发送之前,添加header
Request newRequest = originalRequest.newBuilder()
.header("Authorization", "test authorization")
.addHeader("Custom-Header", "test custom header")
.build();

// 执行chain.proceed(Request)时
// 会把请求交给责任链中的下个拦截器处理,并且返回下个拦截器的请求响应
// 这里把请求结果返回给上个拦截器
return chain.proceed(newRequest);
}
}

如下拦截器,用于记录请求耗时和输出响应体内容

ResponseBody的数据流只能被消费一次,若需多次读取,可用response.peekBody()获取一个副本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class LoggingInterceptor implements Interceptor {
private static final String TAG = LoggingInterceptor.class.getSimpleName();
@NonNull
@Override
public Response intercept(@NonNull Chain chain) throws IOException {
Request request = chain.request();

// 记录请求发送之前的时间
long startTime = System.nanoTime();

// 把请求递交给下一责任链,并拿到下一责任链返回的响应
Response response = chain.proceed(request);

// 计算请求耗时,输出
long duration = System.nanoTime() - startTime;
LogUtil.d(TAG, "请求耗时: " + duration);

// 响应体只能读取一次,使用peekBody获取副本避免影响后续处理
ResponseBody peekBody = response.peekBody(Long.MAX_VALUE);

// 输出Body
LogUtil.d(TAG, "Body: " + peekBody.string());

// 把响应返回给责任链中上个拦截器
return response;
}
}

添加拦截器到OkHttpClient

1
2
3
4
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.addInterceptor(new HeaderInterceptor())
.addInterceptor(new LoggingIntercepter())
.build();

网络拦截器

Volley

Retrofit

Retrofit本身不执行网络请求,只负责封装请求和解析结果,真正的网络通信由OkHttp负责

发送get请求

先根据API返回的json数据结构