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 版本是不會加入的,所以也不能直接寫死忽略第一次寫入的統計。
最終,我發現日誌攔截器中的 BufferedSink
是 Buffer
型別,而實際進行網路請求的 BufferedSink
是 FixedLengthSink
。所以修改 ProgressRequestBody
裡的 writeTo(BufferedSink sink)
方法,如果傳入的 sink
為 Buffer
物件,則直接寫入,不進行統計,程式碼如下:
@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();
}
執行,解決。