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日志并进行多维度分析。
目录
相关文章
|
6月前
|
API 数据库
Activiti 流程资源文件下载及历史信息查询
Activiti 流程资源文件下载及历史信息查询
56 0
|
6月前
axios 上传显示进度
axios 上传显示进度
107 0
|
1月前
|
前端开发 JavaScript
💥【exceljs】纯前端如何实现Excel导出下载和上传解析?
本文介绍了用于处理Excel文件的库——ExcelJS,相较于SheetJS,ExcelJS支持更高级的样式自定义且易于使用。表格对比显示,ExcelJS在样式设置、内存效率及流式操作方面更具优势。主要适用于Node.js环境,也支持浏览器端使用。文中详细展示了如何利用ExcelJS实现前端的Excel导出下载和上传解析功能,并提供了示例代码。此外,还提供了在线调试的仓库链接和运行命令,方便读者实践。
291 5
|
4月前
|
JavaScript
文本,视频网站提交的方法,配置提交的按钮,一次性提交的方法,Vue编写好代码向后台发送数据就行
文本,视频网站提交的方法,配置提交的按钮,一次性提交的方法,Vue编写好代码向后台发送数据就行
|
6月前
|
JSON Rust 前端开发
【sheetjs】纯前端如何实现Excel导出下载和上传解析?
本文介绍了如何使用`sheetjs`的`xlsx`库在前端实现Excel的导出和上传。项目依赖包括Vite、React、SheetJS和Arco-Design。对于导出,从后端获取JSON数据,通过`json_to_sheet`、`book_new`和`writeFile`函数生成并下载Excel文件。对于上传,使用`read`函数将上传的Excel文件解析为JSON并发送至后端。完整代码示例可在GitHub仓库[fullee/sheetjs-demo](https://github.com/fullee/sheetjs-demo)中查看。
419 10
|
6月前
|
前端开发
elementui-upload组件自定义样式上传(upload中常用的属性,但是网络上却找不到教程)(解决bug删除之后再次上传会上传删除的图片)专注后端工程师的前端速成
elementui-upload组件自定义样式上传(upload中常用的属性,但是网络上却找不到教程)(解决bug删除之后再次上传会上传删除的图片)专注后端工程师的前端速成
705 0
|
前端开发 小程序
钉钉小程序 上传一百张图片的base64给接口该怎么写好,正常写的话应该是前端文件限制了长度,只要太大后面一些全部为null
钉钉小程序 上传一百张图片的base64给接口该怎么写好,正常写的话应该是前端文件限制了长度,只要太大后面一些全部为null
|
存储 缓存 前端开发
【Java项目】bitmap实现B站点赞超过500取消最早的点赞记录的实现思路
【Java项目】bitmap实现B站点赞超过500取消最早的点赞记录的实现思路
182 0
|
存储 API 开发工具
Bug日志(三)-获取本地图片后上传失败
原来为了能给用户提供对文件的更多控制并限制文件混乱,Android Q改变了应用程序访问设备外部存储上文件的方式,例如存储在路径/ sdcard中的文件。Android Q继续使用READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE权限,这些权限对应于存储面向用户的运行时权限。但是,默认情况下targetSdkVersion设置为Android Q的应用(以及manifest清单开启属性来启动这个变更的应用)会获得一个沙盒视图到外部存储。此类应用程序只能看到其特定于应用程序的目录和特定媒体类型,因此应用程序不需要请求任何其他用户权限。
|
前端开发 数据库
eggjs 怎么实现更新用户信息接口去更新上传头像信息?
eggjs 怎么实现更新用户信息接口去更新上传头像信息?
160 0
 eggjs 怎么实现更新用户信息接口去更新上传头像信息?