Android 大文件分块上传实战:突破表单数据限制的完整方案

一、问题背景与核心思路

1.1 场景痛点

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

1.2 核心思路

分块上传 + 服务端合并

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

二、Android 客户端实现细节

2.1 分块处理与上传流程

完整代码实现(Kotlin)

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) // 重试逻辑
                }
            }
        }
    }

    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)确保唯一性

    kotlin 复制代码
    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 记录上传进度

    kotlin 复制代码
    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 网络请求实现(Retrofit + Kotlin Coroutine)

kotlin 复制代码
// 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 接收分块接口

java 复制代码
@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, int totalChunks) {
        // 实现合并逻辑
    }
}

3.2 合并文件实现

java 复制代码
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. 服务端优化

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

分块校验示例(服务端)

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

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

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

  1. 阿里云OSS分片上传

    kotlin 复制代码
    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)
  2. AWS S3 TransferUtility

    java 复制代码
    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());

八、关键点总结

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

九、参考资源

  1. Retrofit 官方文档
  2. Spring Boot 文件上传指南
  3. AWS Android SDK 文档
  4. MD5校验最佳实践
相关推荐
阿冲Runner34 分钟前
创建一个生产可用的线程池
java·后端
写bug写bug43 分钟前
你真的会用枚举吗
java·后端·设计模式
喵手1 小时前
如何利用Java的Stream API提高代码的简洁度和效率?
java·后端·java ee
-Xie-1 小时前
Maven(二)
java·开发语言·maven
whysqwhw1 小时前
安卓图片性能优化技巧
android
IT利刃出鞘2 小时前
Java线程的6种状态和JVM状态打印
java·开发语言·jvm
风往哪边走2 小时前
自定义底部筛选弹框
android
天天摸鱼的java工程师2 小时前
Java 解析 JSON 文件:八年老开发的实战总结(从业务到代码)
java·后端·面试
白仑色2 小时前
Spring Boot 全局异常处理
java·spring boot·后端·全局异常处理·统一返回格式
Yyyy4822 小时前
MyCAT基础概念
android