因产品需求,需要实现图片上传显示文件进度。我在项目中是使用的 Retrofit 和 RxJava,虽网上不乏相关文章,然而在使用的过程中还是遇到了点坑,记录为文,谨供他人参考。
实现
我在项目中使用的是 RxJava + Retrofit + OkHttp,网上不乏此类实现上传文件进度的文章,我找到的是《再谈Retrofit:文件的上传下载及进度显示》与《RxJava2+Retrofit2单文件上传监听进度封装(服务端代码+客户端代码)》。这两篇的实现方式都是一样的,即通过继承 RequestBody,对原有的 RequestBody 进行包装,通过重写写入数据的 public void writeTo(BufferedSink sink) throws IOException
方法对所传入的 BufferedSink
对象进行包装,然后通过继承 ForwardingSink
重写 public void write(Buffer source, long byteCount) throws IOException
方法,从而实现对写入数据的统计,再获取数据总长度,就可以实时获取进度了。
参考其中一篇文章,略作修改,由于这里已经使用了 rxjava,所以便使用 Emitter
来提交进度,并封装了个表示上传进度的对象,最终实现如下。
对 RequestBody 进行封装,实现上传数据统计:
class ProgressRequestBody extends RequestBody {
private RequestBody mDelegate;
private Emitter<UploadProgressInfo> mEmitter;
private UploadProgressInfo mProgressInfo;
private BufferedSink mBufferedSink;
ProgressRequestBody(RequestBody delegate, Emitter<UploadProgressInfo> emitter,
UploadProgressInfo info) {
mDelegate = delegate;
mEmitter = emitter;
mProgressInfo = info;
}
@Override
public long contentLength() throws IOException {
return mDelegate.contentLength();
}
@Override
public MediaType contentType() {
return mDelegate.contentType();
}
@Override
public void writeTo(BufferedSink sink) throws IOException {
if (mBufferedSink == null) {
mBufferedSink = Okio.buffer(wrapSink(sink));
}
mDelegate.writeTo(mBufferedSink);
mBufferedSink.flush();
}
private Sink wrapSink(Sink sink) {
return new ForwardingSink(sink) {
@Override
public void write(Buffer source, long byteCount) throws IOException {
super.write(source, byteCount);
if (mProgressInfo.total == 0) {
mProgressInfo.total = contentLength();
}
mProgressInfo.current += byteCount;
mEmitter.onNext(mProgressInfo);
}
};
}
}
Retrofit 接口声明,参数为 @Body RequestBody body
:
public interface UploadService {
/**
* 上传图片
*
* @param body 请求体
* @return Observable
*/
@POST("/upload")
Observable<UploadResponse> upload(@Body RequestBody body);
}
调用:
public void uploadPhotoFile(final CertificateType type, final File file) {
Observable.create(new Action1<Emitter<UploadProgressInfo>>() {
@Override
public void call(Emitter<UploadProgressInfo> emitter) {
doUpload(type, file, emitter);
}
}, Emitter.BackpressureMode.LATEST)
.onBackpressureLatest()
.subscribeOn(AndroidSchedulers.mainThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(watchSubscriber(new RxAction<UploadProgressInfo>() {
@Override
public void onNext(UploadProgressInfo info) {
getView().onUploading(info);
}
@Override
public void onError(Throwable e) {
super.onError(e);
getView().onUploadFailure(type);
}
}));
}
其中 private void doUpload(final CertificateType type, File file, final Emitter<UploadProgressInfo> emitter)
方法主要代码如下:
final UploadParams params = new UploadParams(file);
final RequestBody fileOriginalBody = BodyUtil.createMultipartBody(params);
UPLOAD_SERVICE.upload(new ProgressRequestBody(fileOriginalBody, emitter, info))
.compose(this.<UploadResponse>applySchedulers())
//代码略
遇坑
然而运行之后,我有点懵了。上传进度一下子就 100%,然后继续慢慢涨,一直涨到 200%,然后提示上传失败。
反复对比文章中的代码,确定我没写错,但却得不到同样的结果。
看了一下上传失败所报的异常如下:
java.net.ProtocolException: unexpected end of stream
at okhttp3.internal.http1.Http1Codec$FixedLengthSink.close(Http1Codec.java:298)
at okio.RealBufferedSink.close(RealBufferedSink.java:236)
at okhttp3.internal.http.CallServerInterceptor.intercept(CallServerInterceptor.java:63)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:92)
at okhttp3.internal.connection.ConnectInterceptor.intercept(ConnectInterceptor.java:45)
...
我又按另一篇文章的写法改了一下,把包装 BufferedSink
的成员变量 mBufferedSink
改成了局部变量:
@Override
public void writeTo(BufferedSink sink) throws IOException {
BufferedSink bufferedSink = Okio.buffer(wrapSink(sink));
mDelegate.writeTo(bufferedSink);
bufferedSink.flush();
}
这时,发现日志提示上传成功了,但是上传进度还是 200%。
原因及解决
被这个问题困扰折腾许久,最终我发现了原因。原来,我这边在 debug 版本会打印所有网络请求的日志,以便调试及查问题。打印日志的方式是通过添加一个 OkHttp 的拦截器,然后把请求及响应的内容打印处理。打印日志的拦截器,是参考 OkHttp 的 LoggingInterceptor
修改而来,其中获取请求的内容是通过创建一个 Buffer
对象,把请求体写到这个对象中,代码如下:
Buffer buffer = new Buffer();
requestBody.writeTo(buffer);
对于上传文件,也就是在真正的上传前,其 writeTo(BufferedSink sink)
方法会被调用一次,用于打印日志,在之后又会被调用一次,用于真正的上传。所以上传进度会是 200%。而第一次是直接写入到 buffer 对象中,所以会很快,所以一下子就先 100%。
原因是找到了,那如何解决?
首先,这个日志拦截器是不能去掉的,因为在开发中有时遇到网络请求的相关问题,就需要查看日志看是参数不对还是服务端返回有问题。
其次,这个日志拦截器在只会在 debug 版本,以及测试环境版本中加入,在正式环境的 release 版本是不会加入的,所以也不能直接写死忽略第一次写入的统计。
最终,我发现日志拦截器中的 BufferedSink
是 Buffer
类型,而实际进行网络请求的 BufferedSink
是 FixedLengthSink
。所以修改 ProgressRequestBody
里的 writeTo(BufferedSink sink)
方法,如果传入的 sink
为 Buffer
对象,则直接写入,不进行统计,代码如下:
@Override
public void writeTo(BufferedSink sink) throws IOException {
if (sink instanceof Buffer) {
// Log Interceptor
mDelegate.writeTo(sink);
return;
}
if (mBufferedSink == null) {
mBufferedSink = Okio.buffer(wrapSink(sink));
}
mDelegate.writeTo(mBufferedSink);
mBufferedSink.flush();
}
运行,解决。