RxJava加Retrofit文件分段上傳實(shí)現(xiàn)詳解
前言
本文基于 RxJava 和 Retrofit 庫(kù),設(shè)計(jì)并實(shí)現(xiàn)了一種用于大文件分塊上傳的工具,并對(duì)其進(jìn)行了全面的拆解分析。拋磚引玉,對(duì)同樣有處理文件分塊上傳訴求的讀者,可能會(huì)起到一定的啟發(fā)作用。
文章主體由四部分構(gòu)成:
- 首先分析問(wèn)題,問(wèn)題拆解為:多線程分段讀取文件、構(gòu)建和發(fā)出文件片段上傳請(qǐng)求
- 基于 JDK 隨機(jī)讀取文件的類庫(kù),設(shè)計(jì)本地多線程分段讀取文件的單元
- 基于 Retrofit 設(shè)計(jì)由文件片段構(gòu)建上傳的網(wǎng)絡(luò)請(qǐng)求
- 從上述設(shè)計(jì)演變而來(lái)的完整代碼實(shí)現(xiàn)
另外,在文章提供的完整代碼中,還附了一段由 PHP 編寫(xiě),用來(lái)接收多線程分段數(shù)據(jù)的服務(wù)端接口實(shí)現(xiàn),其中處理了因客戶端都線程上傳片段,導(dǎo)致服務(wù)端接收的文件片段無(wú)序,故需在適當(dāng)時(shí)機(jī)合并分塊構(gòu)成目標(biāo)文件。
受限于筆者的開(kāi)發(fā)經(jīng)驗(yàn)與理論理解,文章的思路和代碼難免可能有偏頗,對(duì)于有改進(jìn)和優(yōu)化的部分,歡迎大家討論區(qū)提出。
問(wèn)題拆解
要完成文件分段上傳到服務(wù)端,第一步是分段讀取本地文件。通常分段是為了多線程同時(shí)執(zhí)行上傳,提高設(shè)備計(jì)算和網(wǎng)絡(luò)資源利用率,減少上傳時(shí)間優(yōu)化體驗(yàn),這樣即需要一個(gè)支持多線程的文件分段讀取工具。由于文件可能超過(guò)設(shè)備內(nèi)存大小,在讀取這類超大文件時(shí)需要控制最大讀取量防止內(nèi)存溢出。此時(shí)文件已從磁盤(pán)數(shù)據(jù)轉(zhuǎn)換為內(nèi)存中的字節(jié)數(shù)據(jù),只需要將這些內(nèi)存數(shù)據(jù)傳給服務(wù)端即可。這樣問(wèn)題被分成 3 個(gè)子問(wèn)題:
- 分段讀取文件到內(nèi)存中
- 控制多線程數(shù)量
- 將文件片段傳給服務(wù)端
問(wèn)題 1 很好解決,利用 Java 的 RandomAccessFile
可對(duì)文件的隨機(jī)讀取的特性,即可按需讀取文件片段到內(nèi)存中。
問(wèn)題 2 相對(duì)復(fù)雜一點(diǎn),但如果有閱讀過(guò) JDK 中線程池源碼的讀者,就會(huì)發(fā)現(xiàn)這個(gè)問(wèn)題的和控制線程池中線程數(shù)量其實(shí)是類似的。
問(wèn)題 3 就不復(fù)雜了,Retrofit 基于 OKhttp ,OkHttp是很容易基于字節(jié)數(shù)組構(gòu)建 multipart/form-data
請(qǐng)求的。
分塊并發(fā)讀取文件
根據(jù)上述對(duì)問(wèn)題 1、2 的拆解,可將讀取抽象為一個(gè)文件讀取器,構(gòu)建時(shí)傳入文件對(duì)象和分段大小以及最大并發(fā)數(shù),以及分段數(shù)據(jù)的回調(diào)。當(dāng)外部啟動(dòng)讀取時(shí)將根據(jù)文件大小和配置的分段大小構(gòu)建若干個(gè) Task 用于讀取對(duì)應(yīng)片段的數(shù)據(jù)。
public BlockReader(@NotNull File file, @NotNull BlockCallback callback, int poolSize, int blockSize) { mFile = file; mCallback = callback; mPoolSize = poolSize; mBlockSize = blockSize; } public void start(@Nullable BlockFilter filter) { Observable.empty().observeOn(Schedulers.computation()).doOnComplete(() -> { long length = mFile.length(); for (long offset = 0; offset < length; offset += mBlockSize) { if (null != filter && filter.ignore(offset)) { continue; } mQueue.offer(new ReadTask(offset)); } for (int i = 0; i < Math.min(mPoolSize, mQueue.size()); i++) { Observable.empty().observeOn(Schedulers.io()).doOnComplete(this::schedule).subscribe(); } }).subscribe(); }
多線程調(diào)度部分,可通過(guò)加鎖和記錄狀態(tài)變量統(tǒng)計(jì)當(dāng)前正運(yùn)行的線程數(shù),則可控制字節(jié)數(shù)組數(shù),這樣就相當(dāng)于控制住了最大內(nèi)存占用。
private void schedule() { if (mRunning.get() >= mPoolSize) { return; } ReadTask task; synchronized (mQueue) { if (mRunning.get() >= mPoolSize) { return; } task = mQueue.poll(); if (null != task) { mRunning.incrementAndGet(); } } if (null != task) { task.run(); } }
最后是文件隨機(jī)讀取,直接調(diào)用 RandomAccessFile
的 API 即可:
private class ReadTask implements Action { @Override public void run() { try (RandomAccessFile raf = new RandomAccessFile(mFile, RAF_MODE); ByteArrayOutputStream out = new ByteArrayOutputStream(mBlockSize)) { raf.seek(mOffset); byte[] buf = new byte[DEF_BLOCK_SIZE]; long cnt = 0; for (int bytes = raf.read(buf); bytes != -1 && cnt < mBlockSize; bytes = raf.read(buf)) { out.write(buf, 0, bytes); cnt += bytes; } out.flush(); mCallback.onFinished(mOffset, out.toByteArray()); } catch (IOException e) { mCallback.onFinished(mOffset, null); } finally { mRunning.decrementAndGet(); schedule(); } } }
文件片段上傳
上傳部分則使用 Retrofit 提供的注解和 OKHttp 的類庫(kù)構(gòu)建請(qǐng)求。但值得一提的是需要在磁盤(pán)IO線程同步完成網(wǎng)絡(luò)IO,這樣可以避免網(wǎng)絡(luò)IO速度落后磁盤(pán)IO太多而導(dǎo)致任務(wù)堆積造成內(nèi)存溢出。
public interface BlockUploader { @POST("test/upload.php") @Multipart Single<Response<ResponseBody>> upload(@Header("filename") String filename, @Header("total") long total, @Header("offset") long offset, @Part List<MultipartBody.Part> body); } private static void syncUpload(String fileName, long fileLength, long offset, byte[] bytes) { RequestBody data = RequestBody.create(MediaType.parse("application/octet-stream"), bytes); MultipartBody body = new MultipartBody.Builder() .addFormDataPart("file", fileName, data) .setType(MultipartBody.FORM) .build(); retrofit.create(BlockUploader.class).upload(fileName, fileLength, offset, body.parts()).subscribe(resp -> { if (resp.isSuccessful()) { System.out.println("? offset: " + offset + " upload succeed " + resp.code()); } else { System.out.println("? offset: " + offset + " upload failed " + resp.code()); } }, throwable -> { System.out.println("! offset: " + offset + " upload failed"); }); }
完整代碼
為控制篇幅,完整代碼請(qǐng)移步 Github,服務(wù)端部分處理形如:
以上就是RxJava加Retrofit文件分段上傳示例的詳細(xì)內(nèi)容,更多關(guān)于RxJava Retrofit文件上傳的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android中實(shí)現(xiàn)Runnable接口簡(jiǎn)單例子
這篇文章主要介紹了Android中實(shí)現(xiàn)Runnable接口簡(jiǎn)單例子,著重點(diǎn)在如何實(shí)現(xiàn)run()方法,需要的朋友可以參考下2014-06-06關(guān)于Android內(nèi)存緩存LruCache的使用及其源碼解析
LruCache作為內(nèi)存緩存,使用強(qiáng)引用方式緩存有限個(gè)數(shù)據(jù),當(dāng)緩存的某個(gè)數(shù)據(jù)被訪問(wèn)時(shí),它就會(huì)被移動(dòng)到隊(duì)列的頭部,本文詳細(xì)介紹了關(guān)于Android內(nèi)存緩存LruCache的使用及其源碼解析,需要的朋友可以參考下2023-05-05Android實(shí)現(xiàn)GridView中的item自由拖動(dòng)效果
在前一個(gè)項(xiàng)目中,實(shí)現(xiàn)了一個(gè)功能是gridview中的item自由拖到效果,實(shí)現(xiàn)思路很簡(jiǎn)單,主要工作就是交換節(jié)點(diǎn),以及拖動(dòng)時(shí)的移動(dòng)效果,下面小編給大家分享具體實(shí)現(xiàn)過(guò)程,對(duì)gridview實(shí)現(xiàn)拖拽效果感興趣的朋友一起看看吧2016-11-11學(xué)習(xí)Android Material Design(RecyclerView代替ListView)
Android Material Design越來(lái)越流行,以前很常用的 ListView 現(xiàn)在也用RecyclerView代替了,實(shí)現(xiàn)原理還是相似的,感興趣的小伙伴們可以參考一下2016-01-01Android學(xué)習(xí)之BottomSheetDialog組件的使用
BottomSheetDialog是底部操作控件,可在屏幕底部創(chuàng)建一個(gè)支持滑動(dòng)關(guān)閉視圖。本文將通過(guò)示例詳細(xì)講解它的使用,感興趣的小伙伴可以了解一下2022-06-06android studio 3.6.1升級(jí)后如何處理 flutter問(wèn)題
這篇文章主要介紹了android-studio-3.6.1升級(jí)后 flutter問(wèn)題,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-03-03Flutter 自定義Drawer 滑出位置的大小實(shí)例代碼詳解
這篇文章主要介紹了Flutter 自定義Drawer 滑出位置的大小,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-04-04Android中操作SQLite數(shù)據(jù)庫(kù)快速入門(mén)教程
這篇文章主要介紹了Android中操作SQLite數(shù)據(jù)庫(kù)快速入門(mén)教程,本文講解了數(shù)據(jù)庫(kù)基礎(chǔ)概念、Android平臺(tái)下數(shù)據(jù)庫(kù)相關(guān)類、創(chuàng)建數(shù)據(jù)庫(kù)、向表格中添加數(shù)據(jù)、從表格中查詢記錄等內(nèi)容,需要的朋友可以參考下2015-03-03Android實(shí)現(xiàn)自定義加載框的代碼示例
本篇文章主要介紹了Android實(shí)現(xiàn)自定義加載框的代碼示例,App在與服務(wù)器進(jìn)行網(wǎng)絡(luò)交互的時(shí)候,有個(gè)提示加載框,有興趣的可以了解一下。2017-02-02