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日志并进行多维度分析。
目录
相关文章
|
2月前
|
前端开发 JavaScript
💥【exceljs】纯前端如何实现Excel导出下载和上传解析?
本文介绍了用于处理Excel文件的库——ExcelJS,相较于SheetJS,ExcelJS支持更高级的样式自定义且易于使用。表格对比显示,ExcelJS在样式设置、内存效率及流式操作方面更具优势。主要适用于Node.js环境,也支持浏览器端使用。文中详细展示了如何利用ExcelJS实现前端的Excel导出下载和上传解析功能,并提供了示例代码。此外,还提供了在线调试的仓库链接和运行命令,方便读者实践。
375 5
|
5月前
|
JavaScript
文本,视频网站提交的方法,配置提交的按钮,一次性提交的方法,Vue编写好代码向后台发送数据就行
文本,视频网站提交的方法,配置提交的按钮,一次性提交的方法,Vue编写好代码向后台发送数据就行
|
7月前
|
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)中查看。
444 10
|
7月前
|
前端开发
elementui-upload组件自定义样式上传(upload中常用的属性,但是网络上却找不到教程)(解决bug删除之后再次上传会上传删除的图片)专注后端工程师的前端速成
elementui-upload组件自定义样式上传(upload中常用的属性,但是网络上却找不到教程)(解决bug删除之后再次上传会上传删除的图片)专注后端工程师的前端速成
802 0
|
存储 开发工具 git
二十四.SpringCloudConfig源码-配置拉取流程
这篇文章是接上一篇的,因为文章太长看起来太累,所以就分了一下
|
开发工具 开发者 iOS开发
​ 如何处理Xcode上传IPA文件后无法在后台架构版本中显示的问题?
1、ipa包符合要求,显示正在处理,这种就是成功上传,等待处理即可(根据包大小,一般处理几分钟到一两个钟都有可能) 2、ipa不符合要求,没有出现正在处理,或者刷新页面正在处理的版本消失 出现这种情况说明你上传的这个ipa包有问题,苹果会发送具体原因到邮箱(开发者账号就是邮箱地址,) 登录邮箱去查看反馈邮件(反馈邮件也可能在垃圾箱),修改错误重新再打包上传。
​  如何处理Xcode上传IPA文件后无法在后台架构版本中显示的问题?
|
Java 定位技术 语音技术
​如何处理Xcode上传IPA文件后无法在后台架构版本中显示的问题?
​如何处理Xcode上传IPA文件后无法在后台架构版本中显示的问题?
|
前端开发 数据库
eggjs 怎么实现更新用户信息接口去更新上传头像信息?
eggjs 怎么实现更新用户信息接口去更新上传头像信息?
163 0
 eggjs 怎么实现更新用户信息接口去更新上传头像信息?
|
前端开发 数据库
eggjs 怎么实现返回 base64 图片的接口给前端回显头像?
eggjs 怎么实现返回 base64 图片的接口给前端回显头像?
283 0
eggjs 怎么实现返回 base64 图片的接口给前端回显头像?
|
存储 JavaScript 安全
教你如何用一行命令:Android打包->上传->发测试包通知
教你如何用一行命令:Android打包->上传->发测试包通知
270 0
教你如何用一行命令:Android打包->上传->发测试包通知