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

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 因产品需求,需要实现图片上传显示文件进度。我在项目中是使用的 Retrofit 和 RxJava,虽网上不乏相关文章,然而在使用的过程中还是遇到了点坑,记录为文,谨供他人参考。实现我在项目中使用的是 RxJava + Retrofit + OkHttp,网上不乏此类实现上传文件进度的文章,我找到的是《再谈Retrofit:文件的上传下载及进度显示》与《RxJava2+Retrofit2单文件上传监听进度封装(服务端代码+客户端代码)》。

因产品需求,需要实现图片上传显示文件进度。我在项目中是使用的 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();
    }

运行,解决。

参考资料

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
5月前
|
Linux Windows
IDEA如何查看每一行代码的提交记录(人员,时间)
【9月更文挑战第24天】在IntelliJ IDEA中,可通过安装GitToolBox插件并利用其功能来便捷地查看每行代码的提交记录,包括提交者、时间和提交信息。具体操作为:首先安装GitToolBox插件,然后在代码编辑区域将鼠标悬停于目标代码行以查看简要信息,或使用快捷键打开“Version Control”窗口查看详细提交历史。
3089 2
|
7月前
|
JavaScript
文本,视频网站提交的方法,配置提交的按钮,一次性提交的方法,Vue编写好代码向后台发送数据就行
文本,视频网站提交的方法,配置提交的按钮,一次性提交的方法,Vue编写好代码向后台发送数据就行
OkHttp3源码详解(三) 拦截器,PDF超过6000页,
OkHttp3源码详解(三) 拦截器,PDF超过6000页,
|
9月前
|
存储 移动开发 AndFix
mPaaS常见问题之pushWindow打开url白屏过一会提示网络超时如何解决
mPaaS(移动平台即服务,Mobile Platform as a Service)是阿里巴巴集团提供的一套移动开发解决方案,它包含了一系列移动开发、测试、监控和运营的工具和服务。以下是mPaaS常见问题的汇总,旨在帮助开发者和企业用户解决在使用mPaaS产品过程中遇到的各种挑战
147 0
uniapp bug记录(后续更新)
uniapp bug记录(后续更新)
135 0
|
JavaScript 前端开发 Java
如何使用Springboot实现文件上传和下载,并为其添加实时进度条的功能
如何使用Springboot实现文件上传和下载,并为其添加实时进度条的功能
442 0
|
存储 API 开发工具
Bug日志(三)-获取本地图片后上传失败
原来为了能给用户提供对文件的更多控制并限制文件混乱,Android Q改变了应用程序访问设备外部存储上文件的方式,例如存储在路径/ sdcard中的文件。Android Q继续使用READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE权限,这些权限对应于存储面向用户的运行时权限。但是,默认情况下targetSdkVersion设置为Android Q的应用(以及manifest清单开启属性来启动这个变更的应用)会获得一个沙盒视图到外部存储。此类应用程序只能看到其特定于应用程序的目录和特定媒体类型,因此应用程序不需要请求任何其他用户权限。
|
前端开发 Linux 程序员
「HTML+CSS」--自定义加载动画【014】【疑问未解决】
「HTML+CSS」--自定义加载动画【014】【疑问未解决】
133 0
|
前端开发 数据库
eggjs 怎么实现更新用户信息接口去更新上传头像信息?
eggjs 怎么实现更新用户信息接口去更新上传头像信息?
174 0
 eggjs 怎么实现更新用户信息接口去更新上传头像信息?
VUE3(二十七)自定义upload上传组件(显示上传进度)
项目中需要使用到文件上传。我这里自己定义了一个上传文件的组件 支持显示上传进度显示(我这里显示的是真实的上传进度,当然,这个可以根据你自己的需求修改)。 支持多文件上传。
472 0
VUE3(二十七)自定义upload上传组件(显示上传进度)