Android實(shí)現(xiàn)大文件分塊上傳的完整方案
一、問題背景與核心思路
1.1 場景痛點(diǎn)
當(dāng) Android 客戶端需要上傳 500MB 的大文件到服務(wù)器,而服務(wù)器表單限制為 2MB 時(shí),傳統(tǒng)的直接上傳方案將完全失效。此時(shí)需要設(shè)計(jì)一套分塊上傳機(jī)制,將大文件拆分為多個(gè)小塊,突破服務(wù)器限制。
1.2 核心思路
分塊上傳 + 服務(wù)端合并:
- 將文件切割為 ≤2MB 的塊
- 逐塊上傳至服務(wù)器
- 服務(wù)端接收后按順序合并
二、Android 客戶端實(shí)現(xiàn)細(xì)節(jié)
2.1 分塊處理與上傳流程
完整代碼實(shí)現(xiàn)(Kotlin)
// FileUploader.kt object FileUploader { // 分塊大小(1.9MB 預(yù)留安全空間) private const val CHUNK_SIZE = 1.9 * 1024 * 1024 suspend fun uploadLargeFile(context: Context, file: File) { val fileId = generateFileId(file) // 生成唯一文件標(biāo)識 val totalChunks = calculateTotalChunks(file) val uploadedChunks = loadProgress(context, fileId) // 加載已上傳分塊記錄 FileInputStream(file).use { fis -> for (chunkNumber in 0 until totalChunks) { if (uploadedChunks.contains(chunkNumber)) continue val chunkData = readChunk(fis, chunkNumber) val isLastChunk = chunkNumber == totalChunks - 1 try { uploadChunk(fileId, chunkNumber, totalChunks, chunkData, isLastChunk) saveProgress(context, fileId, chunkNumber) // 記錄成功上傳的分塊 } catch (e: Exception) { handleRetry(fileId, chunkNumber) // 重試邏輯 } } } } private fun readChunk(fis: FileInputStream, chunkNumber: Int): ByteArray { val skipBytes = chunkNumber * CHUNK_SIZE fis.channel().position(skipBytes.toLong()) val buffer = ByteArray(CHUNK_SIZE) val bytesRead = fis.read(buffer) return if (bytesRead < buffer.size) buffer.copyOf(bytesRead) else buffer } }
關(guān)鍵技術(shù)點(diǎn)解析
1.唯一文件標(biāo)識生成:通過文件內(nèi)容哈希(如 SHA-256)確保唯一性
fun generateFileId(file: File): String { val digest = MessageDigest.getInstance("SHA-256") file.inputStream().use { is -> val buffer = ByteArray(8192) var read: Int while (is.read(buffer).also { read = it } > 0) { digest.update(buffer, 0, read) } } return digest.digest().toHex() }
2.進(jìn)度持久化存儲(chǔ):使用 SharedPreferences 記錄上傳進(jìn)度
private fun saveProgress(context: Context, fileId: String, chunk: Int) { val prefs = context.getSharedPreferences("upload_progress", MODE_PRIVATE) val key = "${fileId}_chunks" val existing = prefs.getStringSet(key, mutableSetOf()) ?: mutableSetOf() prefs.edit().putStringSet(key, existing + chunk.toString()).apply() }
2.2 網(wǎng)絡(luò)請求實(shí)現(xiàn)(Retrofit + Kotlin Coroutine)
// UploadService.kt interface UploadService { @Multipart @POST("api/upload/chunk") suspend fun uploadChunk( @Part("fileId") fileId: RequestBody, @Part("chunkNumber") chunkNumber: RequestBody, @Part("totalChunks") totalChunks: RequestBody, @Part("isLast") isLast: RequestBody, @Part chunk: MultipartBody.Part ): Response<UploadResponse> } // 上傳請求封裝 private suspend fun uploadChunk( fileId: String, chunkNumber: Int, totalChunks: Int, chunkData: ByteArray, isLast: Boolean ) { val service = RetrofitClient.create(UploadService::class.java) val requestFile = chunkData.toRequestBody("application/octet-stream".toMediaType()) val chunkPart = MultipartBody.Part.createFormData( "chunk", "chunk_${chunkNumber}", requestFile ) val response = service.uploadChunk( fileId = fileId.toRequestBody(), chunkNumber = chunkNumber.toString().toRequestBody(), totalChunks = totalChunks.toString().toRequestBody(), isLast = isLast.toString().toRequestBody(), chunk = chunkPart ) if (!response.isSuccessful) { throw IOException("Upload failed: ${response.errorBody()?.string()}") } }
三、服務(wù)端實(shí)現(xiàn)(Spring Boot 示例)
3.1 接收分塊接口
@RestController @RequestMapping("/api/upload") public class UploadController { @Value("${upload.temp-dir:/tmp/uploads}") private String tempDir; @PostMapping("/chunk") public ResponseEntity<?> uploadChunk( @RequestParam String fileId, @RequestParam int chunkNumber, @RequestParam int totalChunks, @RequestParam boolean isLast, @RequestPart("chunk") MultipartFile chunk) { // 創(chuàng)建臨時(shí)目錄 Path tempDirPath = Paths.get(tempDir, fileId); if (!Files.exists(tempDirPath)) { try { Files.createDirectories(tempDirPath); } catch (IOException e) { return ResponseEntity.status(500).body("Create dir failed"); } } // 保存分塊 Path chunkFile = tempDirPath.resolve("chunk_" + chunkNumber); try { chunk.transferTo(chunkFile); } catch (IOException e) { return ResponseEntity.status(500).body("Save chunk failed"); } // 如果是最后一塊則觸發(fā)合并 if (isLast) { asyncMergeFile(fileId, totalChunks); } return ResponseEntity.ok().build(); } @Async public void asyncMergeFile(String fileId, int totalChunks) { // 實(shí)現(xiàn)合并邏輯 } }
3.2 合并文件實(shí)現(xiàn)
private void mergeFile(String fileId, int totalChunks) throws IOException { Path tempDir = Paths.get(this.tempDir, fileId); Path outputFile = Paths.get("/data/final", fileId + ".dat"); try (OutputStream out = new BufferedOutputStream(Files.newOutputStream(outputFile))) { for (int i = 0; i < totalChunks; i++) { Path chunk = tempDir.resolve("chunk_" + i); Files.copy(chunk, out); } out.flush(); } // 清理臨時(shí)文件 FileUtils.deleteDirectory(tempDir.toFile()); }
四、技術(shù)對比與方案選擇
方案 | 優(yōu)點(diǎn) | 缺點(diǎn) | 適用場景 |
---|---|---|---|
傳統(tǒng)表單上傳 | 實(shí)現(xiàn)簡單 | 受限于服務(wù)器大小限制 | 小文件上傳(<2MB) |
分塊上傳 | 突破大小限制,支持?jǐn)帱c(diǎn)續(xù)傳 | 實(shí)現(xiàn)復(fù)雜度較高 | 大文件上傳(>100MB) |
第三方云存儲(chǔ)SDK | 無需自行實(shí)現(xiàn),功能完善 | 依賴第三方服務(wù),可能有費(fèi)用產(chǎn)生 | 需要快速集成云存儲(chǔ)的場景 |
五、關(guān)鍵實(shí)現(xiàn)步驟總結(jié)
1.客戶端分塊切割
- 確定分塊大?。ńㄗh略小于限制值)
- 生成唯一文件ID(基于文件內(nèi)容哈希)
- 實(shí)現(xiàn)可恢復(fù)的上傳進(jìn)度記錄
2.分塊上傳
- 使用多部分表單上傳每個(gè)分塊
- 攜帶分塊元數(shù)據(jù)(序號/總數(shù)/文件ID)
- 實(shí)現(xiàn)超時(shí)重試機(jī)制
3.服務(wù)端處理
- 按文件ID創(chuàng)建臨時(shí)存儲(chǔ)目錄
- 驗(yàn)證分塊完整性(可選MD5校驗(yàn))
- 原子性合并操作
4.可靠性增強(qiáng)
- 斷點(diǎn)續(xù)傳支持
- 網(wǎng)絡(luò)異常自動(dòng)重試
- 上傳完整性校驗(yàn)
六、注意事項(xiàng)與優(yōu)化建議
1.分塊大小優(yōu)化
- 建議設(shè)置為
服務(wù)器限制值 * 0.95
(如 1.9MB) - 測試不同分塊大小對傳輸效率的影響
2.并發(fā)控制
- 可并行上傳多個(gè)分塊(需服務(wù)端支持)
- 合理控制并發(fā)數(shù)(建議 3-5 個(gè)并行)
3.安全防護(hù)
- 添加身份驗(yàn)證(JWT Token)
- 限制單個(gè)文件的最大分塊數(shù)
- 使用 HTTPS 加密傳輸
4.服務(wù)端優(yōu)化
- 設(shè)置合理的臨時(shí)文件清理策略
- 使用異步合并操作避免阻塞請求線程
- 實(shí)現(xiàn)分塊哈希校驗(yàn)(示例代碼見下方)
分塊校驗(yàn)示例(服務(wù)端):
// 計(jì)算分塊MD5 String receivedHash = DigestUtils.md5Hex(chunk.getInputStream()); if (!receivedHash.equals(clientProvidedHash)) { throw new InvalidChunkException("Chunk hash mismatch"); }
七、擴(kuò)展方案:第三方云存儲(chǔ)集成
對于不想自行實(shí)現(xiàn)分塊上傳的場景,可考慮以下方案:
阿里云OSS分片上傳
val oss = OSSClient(context, endpoint, credentialProvider) val request = InitiateMultipartUploadRequest(bucketName, objectKey) val uploadId = oss.initMultipartUpload(request).uploadId // 上傳分片 val partETags = mutableListOf<PartETag>() for (i in chunks.indices) { val uploadPartRequest = UploadPartRequest( bucketName, objectKey, uploadId, i+1).apply { partContent = chunks[i] } partETags.add(oss.uploadPart(uploadPartRequest).partETag) } // 完成上傳 val completeRequest = CompleteMultipartUploadRequest( bucketName, objectKey, uploadId, partETags) oss.completeMultipartUpload(completeRequest)
AWS S3 TransferUtility
TransferUtility transferUtility = TransferUtility.builder() .s3Client(s3Client) .context(context) .build(); MultipleFileUpload upload = transferUtility.uploadDirectory( bucketName, remoteDir, localDir, new ObjectMetadataProvider() { @Override public void provideObjectMetadata(File file, ObjectMetadata metadata) { metadata.setContentType("application/octet-stream"); } }); upload.setTransferListener(new UploadListener());
八、關(guān)鍵點(diǎn)總結(jié)
- 分塊策略:合理設(shè)置分塊大小,生成唯一文件標(biāo)識
- 斷點(diǎn)續(xù)傳:本地持久化上傳進(jìn)度,支持網(wǎng)絡(luò)恢復(fù)
- 完整性校驗(yàn):客戶端與服務(wù)端雙端校驗(yàn)分塊數(shù)據(jù)
- 并發(fā)控制:平衡并行上傳數(shù)量與服務(wù)器壓力
- 錯(cuò)誤處理:實(shí)現(xiàn)自動(dòng)重試與異常上報(bào)機(jī)制
- 安全防護(hù):身份驗(yàn)證 + 傳輸加密 + 大小限制
到此這篇關(guān)于Android實(shí)現(xiàn)大文件分塊上傳的完整方案的文章就介紹到這了,更多相關(guān)Android大文件分塊上傳內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android?Bugreport實(shí)現(xiàn)原理深入分析
這篇文章主要介紹了Android?Bugreport實(shí)現(xiàn)原理,Bugreport主要用于分析手機(jī)的狀態(tài),在應(yīng)用開發(fā)中,程序的調(diào)試分析是日常生產(chǎn)中進(jìn)程會(huì)進(jìn)行的工作,Bugreport就是很常用的工具,需要的朋友可以參考下2024-05-05Android Studio實(shí)現(xiàn)帶邊框的圓形頭像
這篇文章主要為大家詳細(xì)介紹了Android Studio實(shí)現(xiàn)帶邊框的圓形頭像,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-10-10Android實(shí)現(xiàn)平滑翻動(dòng)效果
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)平滑翻動(dòng)效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-04-04Linux系統(tǒng)下安裝android sdk的方法步驟
這篇文章主要介紹了Linux系統(tǒng)下安裝android sdk的方法步驟,文中介紹的非常詳細(xì),相信對大家具有一定的參考價(jià)值,需要的朋友可以們下面來一起看看吧。2017-03-03Android學(xué)習(xí)教程之日歷庫使用(15)
這篇文章主要為大家詳細(xì)介紹了Android學(xué)習(xí)教程之日歷庫使用的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-11-11Android編程實(shí)現(xiàn)隱藏狀態(tài)欄及測試Activity是否活動(dòng)的方法
這篇文章主要介紹了Android編程實(shí)現(xiàn)隱藏狀態(tài)欄及測試Activity是否活動(dòng)的方法,涉及Android界面布局設(shè)置及Activity狀態(tài)操作的相關(guān)技巧,需要的朋友可以參考下2016-10-10