OKHTTP3 简单使用(四) 上传文件

文件上传基于POST,因此和POST写法基本一致,但是不再使用FormBody,FormBody使用的MideaType(Content-Type)为"application/x-www-form-urlencoded; charset=utf-8",无法上传文件,而上传文件需要通过"application/form-data; charset=utf-8"的方式进行

OkHttp还提供了一个MultipartBody类,同样继承自RequestBody,可以以"application/form-data; charset=utf-8"进行文件上传,同时"application/form-data"不仅可以上传文件,也可以用于普通数据的提交

普通参数提交

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
OkHttpClient client = new OkHttpClient();
MultipartBody multipartBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("param1", "value1")
.addFormDataPart("param2", "value2")
.addFormDataPart("param3", "value3")
.build();
Request request = new Request.Builder()
.url(url)
.post(multipartBody)
.build();
Call call = client.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}

@Override
public void onResponse(Call call, Response response) throws IOException {
String result = response.body().string();
}
}

MultipartBody在普通参数提交的时候,使用方式与FormBody基本一样,不过MultipartBody请求需要设置ContentType

文件上传

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
OkHttpClient client = new OkHttpClient();
File file = new File("File path");
RequestBody requestBody = RequestBody.create(MediaType.parse("application/octet-stream"), file);
MultipartBody multipartBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("param1", "value1")
.addFormDataPart("param2", "value2")
.addFormDataPart("param3", "value3")
.addFormDataPart("pic", "1.png", requestBody)
.build();
Request request = new Request.Builder()
.url(url)
.post(multipartBody)
.build();
Call call = client.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}

@Override
public void onResponse(Call call, Response response) throws IOException {
String result = response.body().string();
}
}

文件依然是通过addFormDataPart进行添加,只不过多了一个RequestBody参数
将文件存放到RequestBody中,然后将该RequestBody添加到MultipartBody中,组成一个完整的RequestBody用于请求
“application/octet-stream”是以文件流方式传输,适合于所有文件,也可以使用指定的Content-Type进行传输,比如"application/x-png"

通过MultipartBody就可以轻松将文件上传至服务器,无需进行复杂的流操作,但是美中不足的是没有进度信息,如果文件较大的情况下,上传耗时比较久,只有文件完全上传后才有回调,十分不友好

文件上传-进度

OkHttp并没有提供进度的相关回调或者接口,但是POST都是通过RequestBody进行请求的,查看RequestBody类就可以发现RequestBody只有三个主要方法

1
2
3
4
5
6
7
8
9
10
11
12
13
/** Returns the Content-Type header for this body. */
public abstract @Nullable MediaType contentType();

/**
* Returns the number of bytes that will be written to {@code sink} in a call to {@link #writeTo},
* or -1 if that count is unknown.
*/
public long contentLength() throws IOException {
return -1;
}

/** Writes the content of this request to {@code sink}. */
public abstract void writeTo(BufferedSink sink) throws IOException;

根据注释可以明确各个方法的作用

  1. contentType() 返回Content-Type
  2. contentLength() 返回已写入数据大小
  3. writeTo 将数据写入BufferedSink

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
/** Returns a new request body that transmits the content of {@code file}. */
public static RequestBody create(final @Nullable MediaType contentType, final File file) {
if (file == null) throw new NullPointerException("file == null");

return new RequestBody() {
@Override public @Nullable MediaType contentType() {
return contentType;
}

@Override public long contentLength() {
return file.length();
}

@Override public void writeTo(BufferedSink sink) throws IOException {
Source source = null;
try {
source = Okio.source(file);
sink.writeAll(source);
} finally {
Util.closeQuietly(source);
}
}
};
}

因此只要获取BufferedSink的大小就可以获取文件传输的大小,但是OkHttp提供的方法中是将文件通过writeAll()进行写入的,无法确定每个时间段具体传输了多少,继续查看writeAll()

1
2
3
4
5
6
7
8
@Override public long writeAll(Source source) throws IOException {
if (source == null) throw new IllegalArgumentException("source == null");
long totalBytesRead = 0;
for (long readCount; (readCount = source.read(this, Segment.SIZE)) != -1; ) {
totalBytesRead += readCount;
}
return totalBytesRead;
}

到这就很明了了,writeAll每次写入的是Segment.SIZE(8192)字节,我们只要将writeAll提取出来就可以自己控制上传的进度了

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
public class ProgressRequestBody extends RequestBody {
private MediaType contentType;
private File file;
private ProgressCallback mCallback;

public ProgressRequestBody(MediaType contentType, File file, ProgressCallback mCallback) {
this.contentType = contentType;
this.file = file;
this.mCallback = mCallback;
}
@Override public @Nullable MediaType contentType() {
return contentType;
}

@Override public long contentLength() {
return file.length();
}

@Override public void writeTo(BufferedSink sink) throws IOException {
Source source = null;
try {
source = Okio.source(file);
long totalBytesRead = 0;
for (long readCount; (readCount = source.read(this, Segment.SIZE)) != -1; ) {
totalBytesRead += readCount;
mCallback.uploadProgress(totalBytesRead);
}
} finally {
Util.closeQuietly(source);
}
}

public interface ProgressCallback{
void uploadProgress(long totalBytesRead);
}
}

调用的时候也只需要将

1
RequestBody requestBody = RequestBody.create(MediaType.parse("application/octet-stream"), file);

替换成

1
RequestBody requestBody = new ProgressRequestBody(MediaType.parse("application/octet-stream"), file);

就可以了