Retrofit 上传文件显示进度及踩坑记录

来源:互联网 发布:淘宝楠楠家是洋垃圾 编辑:程序博客网 时间:2024/06/04 20:21

因产品需求,需要实现图片上传显示文件进度。我在项目中是使用的 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 版本是不会加入的,所以也不能直接写死忽略第一次写入的统计。
最终,我发现日志拦截器中的 BufferedSinkBuffer 类型,而实际进行网络请求的 BufferedSinkFixedLengthSink。所以修改 ProgressRequestBody 里的 writeTo(BufferedSink sink) 方法,如果传入的 sinkBuffer 对象,则直接写入,不进行统计,代码如下:

    @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();    }

运行,解决。

参考资料

  • 《再谈Retrofit:文件的上传下载及进度显示》
  • 《RxJava2+Retrofit2单文件上传监听进度封装(服务端代码+客户端代码)》
原创粉丝点击