Android

关注公众号 jb51net

关闭
首页 > 软件编程 > Android > Android大文件分块上传

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

作者:时小雨

这篇文章主要为大家详细介绍了如何使用Android实现大文件分块上传功能,从而突破表单数据限制,文中的示例代码讲解详细,需要的可以了解下

一、问题背景与核心思路

1.1 场景痛点

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

1.2 核心思路

分块上传 + 服务端合并

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

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

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, int 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.客户端分块切割

2.分块上传

3.服务端处理

4.可靠性增强

六、注意事项与优化建议

1.分块大小优化

2.并发控制

3.安全防护

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 = 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());

八、关键点总结

到此这篇关于Android实现大文件分块上传的完整方案的文章就介绍到这了,更多相关Android大文件分块上传内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文