开发者

Android实现大文件分块上传的完整方案

开发者 https://www.devze.com 2025-06-10 10:17 出处:网络 作者: 时小雨
目录一、问题背景与核心思路1.1 场景痛点1.2 核心思路二、android 客户端实现细节2.1 分块处理与上传流程2.2 网络请求实现(Retrofit + Kotlin Corhttp://www.devze.comoutine)三、服务端实现(Spring Boot 示例)3
目录
  • 一、问题背景与核心思路
    • 1.1 场景痛点
    • 1.2 核心思路
  • 二、android 客户端实现细节
    • 2.1 分块处理与上传流程
    • 2.2 网络请求实现(Retrofit + Kotlin Corhttp://www.devze.comoutine)
  • 三、服务端实现(Spring Boot 示例)
    • 3.1 接收分块接口
    • 3.2 合并文件实现
  • 四、技术对比与方案选择
    • 五、关键实现步骤总结
      • 六、注意事项与优化建议
        • 七、扩展方案:第三方云存储集成
          • 八、关键点总结

            一、问题背景与核心思路

            1.1 场景痛点

            当 Android 客户端需要上传 500MB 的大文件到服务器,而服务器表单限制为 2MB 时,传统的直接上传方案将完全失效。此时需要设计一套分块上传机制,将大文件拆分为多个小块,突破服务器限制。

            1.2 核心思路

            分块上传 + 服务端合并

            • 将文件切割为 ≤2MB 的块
            • 逐块上传至服务器
            • 服务端接收后按顺序合并

            二、Android 客户端实现细节

            2.1 分块处理与上传流程

            完整代码实现(Kotlin)

            // FileUploader.kt
            object FileUploader {
                // 分块大小(1.9MB 预留安全空间)
                private const val CHUNK_SIZE = 1.9 * 1024 * 1024 
            
                suspend fun uploadLargeFile(context: Context, file: File) {
                    val fileId = generateFileId(file) // 生成唯一文件标识
                    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) // 重试逻辑HInReUO
                            }
                        }
                    }
                }
            
                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
                }
            }
            

            关键技术点解析

            1.唯一文件标识生成:通过文件内容哈希(如 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.进度持久化存储:使用 SharedPreferences 记录上传进度

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

            2.2 网络请求实现(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()}")
                }
            }
            

            三、服务端实现(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) {
                    
                    // 创建临时目录
                    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");
                    }
                    
                    // 如果是最后一块则触发合并
                    if (isLast) {
                        asyncMergeFile(fileId, totalChunks);
                    }
                    
                    return ResponseEntity.ok().build();
                }
                
                @Async
                public void asyncMergeFile(String fileId, ijavascriptnt totalChunks) {
                    // 实现合并逻辑
                }
            }
            

            3.2 合并文件实现

            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();
                }
                
                // 清理临时文件
                FileUtils.deleteDirectory(tempDir.toFile());
            }
            

            四、技术对比与方案选择

            方案优点缺点适用场景
            传统表单上传实现简单受限于服务器大小限制小文件上传(<2MB)
            分块上传突破大小限制,支持断点续传实现复杂度较高大文件上传(>100MB)
            第三方云存储SDK无需自行实现,功能完善依赖第三方服务,可能有费用产生需要快速集成云存储的场景

            五、关键实现步骤总结

            1.客户端分块切割

            • 确定分块大小(建议略小于限制值)
            • 生成唯一文件ID(基于文件内容哈希)
            • 实现可恢复的上传进度记录

            2.分块上传

            • 使用多部分表单上传每个分块
            • 携带分块元数据(序号/总数/文件ID)
            • 实现超时重试机制

            3.服务端处理

            • 按文件ID创建临时存储目录
            • 验证分块完整性(可选MD5校验)
            • 原子性合并操作

            4.可靠性增强

            • 断点续传支持
            • 网络异常自动重试
            • 上传完整性校验

            六、注意事项与优化建议

            1.分块大小优化

            • 建议设置为 服务器限制值 * 0.95(如 1.9MB)
            • 测试不同分块大小对传输效率的影响

            2.并发控制

            • 可并行上传多个分块(需服务端支持)
            • 合理控制并发数(建议 3-5 个并行)

            3.安全防护

            • 添加身份验证(JWT Token)
            • 限制单个文件的最大分块数
            • 使用 HTTPS 加密传输

            4.服务端优化

            • 设置合理的临时文件清理策略
            • 使用异步合并操作避免阻塞请求线程
            • 实现分块哈希校验(示例代码见下方)

            分块校验示例(服务端)

            // 计算分块MD5
            String receivedHash = DigestUtils.md5Hex(chunk.getInputStream());
            if (!receivedHash.equals(clientProvidedHash)) {
                throw new InvalidChunkException("Chunk hash mismatch");
            }
            

            七、扩展方案:第三方云存储集成

            对于不想自行实现分块上传的场景,可考虑以下方案:

            阿里云OSS分片上传

            val oss = OSSClient(context, endpoint, credentialProvider)
            val request = InitiaandroidteMultipartUploadRequest(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());
            

            八、关键点总结

            • 分块策略:合理设置分块大小,生成唯一文件标识
            • 断点续传:本地持久化上传进度,支持网络恢复
            • 完整性校验:客户端与服务端双端校验分块数据
            • 并发控制:平衡并行上传数量与服务器压力
            • 错误处理:实现自动重试与异常上报机制
            • 安全防护:身份验证 + 传输加密 + 大小限制

            到此这篇关于Android实现大文件分块上传的完整方案的文章就介绍到这了,更多相关Android大文件分块上传内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!

            0

            精彩评论

            暂无评论...
            验证码 换一张
            取 消

            关注公众号