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

 更新时间:2025年06月09日 10:33:34   作者:时小雨  
这篇文章主要为大家详细介绍了如何使用Android实现大文件分块上传功能,从而突破表单数据限制,文中的示例代码讲解详细,需要的可以了解下

一、问题背景与核心思路

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

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

  • 确定分块大小(建议略小于限制值)
  • 生成唯一文件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 = 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大文件分块上传内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Android Bugreport实现原理深入分析

    Android Bugreport实现原理深入分析

    这篇文章主要介绍了Android Bugreport实现原理,Bugreport主要用于分析手机的状态,在应用开发中,程序的调试分析是日常生产中进程会进行的工作,Bugreport就是很常用的工具,需要的朋友可以参考下
    2024-05-05
  • Android EditText禁止输入空格和特殊字符

    Android EditText禁止输入空格和特殊字符

    本文主要介绍了Android EditText禁止输入空格和特殊字符的实现代码。具有很好的参考价值。下面跟着小编一起来看下吧
    2017-04-04
  • Android Studio实现带边框的圆形头像

    Android Studio实现带边框的圆形头像

    这篇文章主要为大家详细介绍了Android Studio实现带边框的圆形头像,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-10-10
  • 基于Android实现3D翻页效果

    基于Android实现3D翻页效果

    这篇文章主要为大家详细介绍了基于Android实现3D翻页效果的具体代码,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2016-06-06
  • Android实现平滑翻动效果

    Android实现平滑翻动效果

    这篇文章主要为大家详细介绍了Android实现平滑翻动效果,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-04-04
  • android设计模式之单例模式详解

    android设计模式之单例模式详解

    这篇文章主要介绍了android设计模式中的单例模式详解,需要的朋友可以参考下
    2014-04-04
  • Linux系统下安装android sdk的方法步骤

    Linux系统下安装android sdk的方法步骤

    这篇文章主要介绍了Linux系统下安装android sdk的方法步骤,文中介绍的非常详细,相信对大家具有一定的参考价值,需要的朋友可以们下面来一起看看吧。
    2017-03-03
  • Android学习教程之日历库使用(15)

    Android学习教程之日历库使用(15)

    这篇文章主要为大家详细介绍了Android学习教程之日历库使用的相关资料,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2016-11-11
  • Android编程实现隐藏状态栏及测试Activity是否活动的方法

    Android编程实现隐藏状态栏及测试Activity是否活动的方法

    这篇文章主要介绍了Android编程实现隐藏状态栏及测试Activity是否活动的方法,涉及Android界面布局设置及Activity状态操作的相关技巧,需要的朋友可以参考下
    2016-10-10
  • Android gradient 使用小结

    Android gradient 使用小结

    在Android中使用gradient(渐变)通常是通过drawable文件来设置背景,下面是可以直接用的几种用法汇总,包括线性渐变、径向渐变、扫描渐变(sweep)等,感兴趣的朋友一起看看吧
    2025-04-04

最新评论