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

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

运行,解决。

参考资料

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在 Kotlin 中使用 Retrofit 进行文件,你可以按照以下步骤进行操作: 1. 首先,确保你已经在项目中添加了 Retrofit 和相应的依赖库。可以在 `build.gradle` 文件中添加以下依赖: ```kotlin implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:converter-gson:2.9.0' implementation 'com.squareup.okhttp3:logging-interceptor:4.9.0' ``` 2. 创建一个接口,定义文件的 API。例如,假设你要上文件到 `/upload` 路径,可以创建一个名为 `FileUploadService` 的接口: ```kotlin import okhttp3.MultipartBody import okhttp3.RequestBody import retrofit2.Call import retrofit2.http.Multipart import retrofit2.http.POST import retrofit2.http.Part interface FileUploadService { @Multipart @POST("/upload") fun uploadFile(@Part file: MultipartBody.Part): Call<ResponseBody> } ``` 3. 创建 Retrofit 实例,并且使用该实例创建 `FileUploadService` 的实例: ```kotlin import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory val retrofit = Retrofit.Builder() .baseUrl("http://your-base-url.com") .client(createOkHttpClient()) .addConverterFactory(GsonConverterFactory.create()) .build() fun createOkHttpClient(): OkHttpClient { val logger = HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY } return OkHttpClient.Builder() .addInterceptor(logger) .build() } val fileUploadService = retrofit.create(FileUploadService::class.java) ``` 4. 现在,你可以使用 `fileUploadService` 实例来上文件。首先,创建一个 `File` 对象,表示要上文件。然后,将文件转换为 `RequestBody` 对象,并使用 `MultipartBody.Part` 将其添加到请求中: ```kotlin import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.MultipartBody import java.io.File val file = File("path/to/your/file") val requestFile = file.asRequestBody("image/*".toMediaTypeOrNull()) val body = MultipartBody.Part.createFormData("file", file.name, requestFile) val call = fileUploadService.uploadFile(body) call.enqueue(object : Callback<ResponseBody> { override fun onResponse(call: Call<ResponseBody>, response: Response<ResponseBody>) { // 文件成功处理 } override fun onFailure(call: Call<ResponseBody>, t: Throwable) { // 文件失败处理 } }) ``` 这样就完成了 Kotlin 中使用 Retrofit 进行文件的过程。记得替换实际的 base URL 和文件路径。希望对你有所帮助!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值