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);

使用步骤很简单

  1. 新建request
  2. 扔到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);
        }
    }
}