1. 程式人生 > >Retrofit 上傳檔案顯示進度及踩坑記錄

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

執行,解決。

參考資料