okhttp上传文件设置进度监听
- 从okhttp用法入手
- 监听数据写入量,代理的编程思想
- 优化使用
- 完整代码
从okhttp用法入手
OkHttpClient client = new OkHttpClient();
RequestBody requestBody = new RequestBody.Builder().build();
Request request = new Request.Builder().post(requestBody).build();
client.newCall(request).enqueue(callback);
使用步骤很简单
- 新建request
- 扔到client中执行
其中request就是http请求对象,包含了完整的http请求报文的内容,header和body。
client把扔进来的request对象进行处理,建立http连接,然后把处理的报文传出去。
http的multipart方式传输文件其实是将文件的二进制流写入报文的body中进行传输。
我们看看okhttp提供的MultipartBody 源码
public final class MultipartBody extends RequestBody {
一堆请求体包含的内容
@Override
public void writeTo(BufferedSink sink) throws IOException {
writeOrCountBytes(sink, false);
}
private long writeOrCountBytes(BufferedSink sink, boolean countBytes) throws IOException {
...
sink.write(一堆请求体包含的内容);
...
}
}
okhttp就是通过writeTo方法将请求体内容组织以okio流的方式写入socket连接流中传输到服务器上的。我们可以从writeTo方法里入手,监听writeTo写多少字节的数据。以此来看文件传输(其实是报文发送)进度。
监听数据写入量,代理的编程思想
首先要去了解一下okio是个神马…
okhttp是用okio来进行流处理的。okio这玩意有个ForwardingSink,这个东西真的奇妙。
public abstract class ForwardingSink implements Sink {
private final Sink delegate;
public ForwardingSink(Sink delegate) {
if (delegate == null) throw new IllegalArgumentException("delegate == null");
this.delegate = delegate;
}
public final Sink delegate() {
return delegate;
}
@Override public void write(Buffer source, long byteCount) throws IOException {
delegate.write(source, byteCount);
}
@Override public void flush() throws IOException {
delegate.flush();
}
@Override public Timeout timeout() {
return delegate.timeout();
}
@Override public void close() throws IOException {
delegate.close();
}
@Override public String toString() {
return getClass().getSimpleName() + "(" + delegate.toString() + ")";
}
}
这个是ForwardingSink 完整的代码,他什么也没做,里面有个Sink类型的delegate对象。ForwardingSink的方法都原封不动的调用了一遍delegate对象的方法。我第一次看完全没搞懂这么做有什么用,就像一个奸商一样,把拦下来的活全交给另一个人干。看了半天我才明白这样写真是精妙。
我们看到MultipartBody中的writeTo方法传入了一个参数BufferedSink sink,okio就是通过sink的write方法写入socket的流,sink对象是个传过来参数,而我们想要监听sink写了多少数据就要重写write方法。
怎么办,我封装一个自定义的sink,继承自这个代理sink类,我们就可以重写write方法了。
class ProgressBufferSink extends ForwardingSink {
ProgressBufferSink(Sink delegate) {
super(delegate);
}
@Override
public void write(Buffer source, long byteCount) throws IOException {
super.write(source, byteCount);
bytesWritten += byteCount;
progressListener.onProgress(bytesWritten, contentLength);
}
}
这样写ProgressBufferSink 还是一个sink,重写自己的write方法做一些其它的事,可以给writeOrCountBytes方法执行,虽然调用的是ProgressBufferSink 的write方法,但最终还是调用的delegate的write方法。
我们也模仿着写一个代理的类 ProgressMultipartRequestBody ,把实际的工作交给MultipartBody来做
public class ProgressMultipartRequestBody extends RequestBody {
private final MultipartBody requestBody;
@Override
public MediaType contentType() {
return requestBody.contentType();
}
@Override
public void writeTo(BufferedSink sink) throws IOException {
...
requestBody.writeTo(bufferedSink);
...
}
...
}
现在就差一步,怎么把ProgressBufferSink当成BufferedSink 传给requestBody.writeTo方法。okio提供了一个静态方法,可以把sink转成bufferSink。
public static BufferedSink buffer(Sink sink)
@Override
public void writeTo(BufferedSink sink) throws IOException {
requestBody.writeTo(Okio.buffer(new ProgressBufferSink(sink)));
}
优化使用
此时使用ProgressMultipartRequestBody 比较麻烦,要新建一个MultipartBody,再封装到ProgressMultipartRequestBody ,再给client去执行。
RequestBody requestBody = new MultipartBody.Builder().build();
ProgressMultipartRequestBody multiRequestBody = new ProgressMultipartRequestBody(requestBody );
Request request = new Request.Builder().post(multiRequestBody )build();
client.newCall(request);
我们可以按照MultipartBody的生产者模式封装一层ProgressMultipartRequestBody生产者模式
public static final class Builder {
private MediaType type = MIXED;
private final List<MultipartBody.Part> parts = new ArrayList<>();
private ProgressListener progressListener;
public Builder() {
}
public Builder setType(MediaType type) {
if (type == null) {
throw new NullPointerException("type == null");
}
if (!type.type().equals("multipart")) {
throw new IllegalArgumentException("multipart != " + type);
}
this.type = type;
return this;
}
public Builder addPart(RequestBody body) {
return addPart(MultipartBody.Part.create(body));
}
public Builder addPart(Headers headers, RequestBody body) {
return addPart(MultipartBody.Part.create(headers, body));
}
public Builder addFormDataPart(String name, String value) {
return addPart(MultipartBody.Part.createFormData(name, value));
}
public Builder addFormDataPart(String name, String filename, RequestBody body) {
return addPart(MultipartBody.Part.createFormData(name, filename, body));
}
public Builder addPart(MultipartBody.Part part) {
if (part == null) throw new NullPointerException("part == null");
parts.add(part);
return this;
}
public Builder addProgressListener(ProgressListener progressListener) {
this.progressListener = progressListener;
return this;
}
public ProgressMultipartRequestBody build() throws IOException, IllegalStateException {
if (parts.isEmpty()) {
throw new IllegalStateException("Multipart body must have at least one part.");
}
if (progressListener == null) {
throw new IllegalStateException("progress listener is null");
}
MultipartBody.Builder builder = new MultipartBody.Builder().setType(type);
for (MultipartBody.Part part : parts) {
builder.addPart(part);
}
return new ProgressMultipartRequestBody(builder.build(), progressListener);
}
}
这样使用上就变成了,在原MultipartBody使用上,将MultipartBody改为ProgressMultipartRequestBody就可以了。
RequestBody requestBody = new ProgressMultipartRequestBody.Builder().build();
Request request = new Request.Builder().post(requestBody).build();
client.newCall(request);
完整代码
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import okhttp3.Headers;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
import okio.Buffer;
import okio.BufferedSink;
import okio.ForwardingSink;
import okio.Okio;
import okio.Sink;
import static okhttp3.MultipartBody.MIXED;
public class ProgressMultipartRequestBody extends RequestBody {
private final MultipartBody requestBody;
private final ProgressListener progressListener;
private BufferedSink bufferedSink;
private long bytesWritten = 0L;
private long contentLength;
private ProgressMultipartRequestBody(MultipartBody requestBody, ProgressListener progressListener) throws IOException {
this.requestBody = requestBody;
this.progressListener = progressListener;
contentLength = requestBody.contentLength();
}
@Override
public MediaType contentType() {
return requestBody.contentType();
}
@Override
public long contentLength() {
return contentLength;
}
@Override
public void writeTo(BufferedSink sink) throws IOException {
if (bufferedSink == null) {
bufferedSink = Okio.buffer(new ProgressBufferSink(sink));
}
requestBody.writeTo(bufferedSink);
bufferedSink.flush();
}
class ProgressBufferSink extends ForwardingSink {
ProgressBufferSink(Sink delegate) {
super(delegate);
}
@Override
public void write(Buffer source, long byteCount) throws IOException {
super.write(source, byteCount);
bytesWritten += byteCount;
progressListener.onProgress(bytesWritten, contentLength);
}
}
public interface ProgressListener {
void onProgress(long currentBytes, long contentLength);
}
public static final class Builder {
private MediaType type = MIXED;
private final List<MultipartBody.Part> parts = new ArrayList<>();
private ProgressListener progressListener;
public Builder() {
}
public Builder setType(MediaType type) {
if (type == null) {
throw new NullPointerException("type == null");
}
if (!type.type().equals("multipart")) {
throw new IllegalArgumentException("multipart != " + type);
}
this.type = type;
return this;
}
public Builder addPart(RequestBody body) {
return addPart(MultipartBody.Part.create(body));
}
public Builder addPart(Headers headers, RequestBody body) {
return addPart(MultipartBody.Part.create(headers, body));
}
public Builder addFormDataPart(String name, String value) {
return addPart(MultipartBody.Part.createFormData(name, value));
}
public Builder addFormDataPart(String name, String filename, RequestBody body) {
return addPart(MultipartBody.Part.createFormData(name, filename, body));
}
public Builder addPart(MultipartBody.Part part) {
if (part == null) throw new NullPointerException("part == null");
parts.add(part);
return this;
}
public Builder addProgressListener(ProgressListener progressListener) {
this.progressListener = progressListener;
return this;
}
public ProgressMultipartRequestBody build() throws IOException, IllegalStateException {
if (parts.isEmpty()) {
throw new IllegalStateException("Multipart body must have at least one part.");
}
if (progressListener == null) {
throw new IllegalStateException("progress listener ");
}
MultipartBody.Builder builder = new MultipartBody.Builder().setType(type);
for (MultipartBody.Part part : parts) {
builder.addPart(part);
}
return new ProgressMultipartRequestBody(builder.build(), progressListener);
}
}
}