当前位置: 首页 > news >正文

【Android】文件分块上传尝试

【Android】文件分块上传

在完成一个项目时,遇到了需要上传长视频的场景,尽管可以手动限制视频清晰度和视频的码率帧率,但仍然避免不了视频大小过大的问题,且由于服务器原因,网络不太稳定。这个时候想到了可以将文件分块。

为什么选择文件分片上传

  1. 提高上传成功率:在网络不稳定或上传大文件时,一般上传可能因网络中断而导致整个上传过程失败,需要重新开始。而分片上传是将文件分成多个小块分别上传,即使某个分片上传失败,只需重新上传该分片,而不是整个文件,大大提高了上传的成功率。
  2. 实现断点续传:分片上传可以记录每个分片的上传进度,当上传因某种原因中断后,再次启动上传时,可以从上次中断的位置继续上传未完成的分片,无需从头开始,节省了时间和带宽。
  3. 并发上传:可以将多个分片同时上传到服务器,利用多线程或并发请求技术,充分利用网络带宽,加快上传速度。特别是对于大文件,并发上传多个分片能够显著缩短上传时间。

文件MD5 摘要计算

public static String getFileMD5String(File file) {MessageDigest messageDigest = null;FileInputStream fileInputStream = null;try {// 1. 初始化 MD5 摘要器messageDigest = MessageDigest.getInstance("MD5");// 2. 打开文件输入流fileInputStream = new FileInputStream(file);byte[] buffer = new byte[8192]; // 8KB 缓冲区int length;// 3. 流式读取文件内容while ((length = fileInputStream.read(buffer)) != -1) {messageDigest.update(buffer, 0, length); // 更新哈希计算}// 4. 生成最终哈希值byte[] digest = messageDigest.digest();// 5. 转换为十六进制字符串StringBuilder sb = new StringBuilder();for (byte b : digest) {String hex = Integer.toHexString(0xFF & b); // 字节转十六进制if (hex.length() == 1) {sb.append('0'); // 补零对齐}sb.append(hex);}return sb.toString();} catch (...) {// 异常处理} finally {// 关闭流}return null;
}

1. 为什么使用 MessageDigest

  • MessageDigest 是 Java 标准库中专门用于生成哈希摘要的类。
  • 支持多种算法(MD5、SHA-1、SHA-256 等),通过 getInstance("MD5") 指定算法。

2. 为什么分块读取(8KB 缓冲区)?

  • 内存效率:直接读取整个文件到内存会导致 OOM(尤其处理大文件时)。
  • 性能平衡
    • 过小(如 1KB)→ 增加 I/O 次数,降低性能。
    • 过大(如 1MB)→ 占用更多内存,边际收益递减。

3. 流式更新的必要性

while ((length = fileInputStream.read(buffer)) != -1) {messageDigest.update(buffer, 0, length);
}
  • 逐块更新哈希状态,避免一次性处理整个文件。
  • 即使文件大小超过内存限制,仍可正常计算。

并发上传

public void sendDetectVideo(File file, String token,String fileMD5String, LoadTasksCallBack callBack) {long fileSize = file.length();int totalChunks = (int) Math.ceil(fileSize * 1.0 / CHUNK_SIZE);CountDownLatch latch = new CountDownLatch(totalChunks);Map<Integer, Future<Boolean>> futures = new HashMap<>();for (int i = 0; i < totalChunks; i++) {final int chunkIndex = i;long start = i * CHUNK_SIZE;long end = Math.min((i + 1) * CHUNK_SIZE, fileSize);Callable<Boolean> task = () -> {try {Log.d("TAG", "sendDetectVideo: " + token);return uploadChunk(file, token, start, end, chunkIndex, totalChunks, fileMD5String, callBack);} finally {latch.countDown();}};Future<Boolean> future = executorService.submit(task);futures.put(chunkIndex, future);}try {latch.await();boolean allSuccess = true;for (Future<Boolean> future : futures.values()) {if (!future.get()) {allSuccess = false;break;}}if (allSuccess) {callBack.onSuccess("所有分块上传成功");} else {callBack.onFailed("部分分块上传失败");}} catch (InterruptedException | ExecutionException e) {e.printStackTrace();callBack.onFailed("上传过程中出现异常: " + e.getMessage());} finally {executorService.shutdown();}
}

初始化参数

long fileSize = file.length();
int totalChunks = (int) Math.ceil(fileSize * 1.0 / CHUNK_SIZE);
  • 计算总分块数:根据文件大小和预设的 CHUNK_SIZE(如5MB)计算需要分成多少块。

并发控制工具

CountDownLatch latch = new CountDownLatch(totalChunks);
Map<Integer, Future<Boolean>> futures = new HashMap<>();
  • CountDownLatch:用于等待所有分块上传完成(初始值为总分块数)。
  • Future集合:保存每个分块上传任务的执行结果。

遍历所有分块

for (int i = 0; i < totalChunks; i++) {final int chunkIndex = i;long start = i * CHUNK_SIZE;long end = Math.min((i + 1) * CHUNK_SIZE, fileSize);Callable<Boolean> task = () -> {try {return uploadChunk(...); // 上传分块} finally {latch.countDown(); // 无论成功与否,计数器减1}};Future<Boolean> future = executorService.submit(task);futures.put(chunkIndex, future);
}
  • 分块范围计算:确定每个分块的起始(start)和结束(end)位置。
  • 任务定义:每个分块上传逻辑封装为 Callable 任务,上传完成后触发 latch.countDown()
  • 任务提交:将任务提交到线程池 executorService,保存返回的 Future 对象。

等待所有分块完成

try {latch.await(); // 阻塞直到所有分块完成// 检查所有任务结果boolean allSuccess = true;for (Future<Boolean> future : futures.values()) {if (!future.get()) { // 获取任务执行结果allSuccess = false;break;}}// 回调结果if (allSuccess) {callBack.onSuccess("所有分块上传成功");} else {callBack.onFailed("部分分块上传失败");}
} catch (...) {// 异常处理
} finally {executorService.shutdown(); // 关闭线程池
}
  • 阻塞等待latch.await() 确保主线程等待所有分块上传完成。
  • 结果检查:遍历所有 Future,检查每个分块是否上传成功。
  • 回调通知:根据结果调用 onSuccessonFailed
  • 资源释放:关闭线程池。

Build请求体

private boolean uploadChunk(File file, String token, long start, long end, int chunkIndex,int totalChunks, String MD5, LoadTasksCallBack callBack) {RequestParams mToken = new RequestParams();mToken.put("Authorization", "Bearer " + token);MultipartBody.Builder requestBody = new MultipartBody.Builder().setType(MultipartBody.FORM);Log.d(TAG, "uploadChunk: " + chunkIndex + " " + totalChunks + " " + MD5);MultipartBody multipartBody = requestBody.addFormDataPart("md5", MD5).addFormDataPart("chunkIndex", String.valueOf(chunkIndex)).addFormDataPart("totalChunks", String.valueOf(totalChunks)).addFormDataPart("file", "file", createChunkRequestBody(file, chunkIndex, start, end)).build();Request request = createRequest(URL.SEND_VIDEO_FILE_URL, multipartBody, mToken, start);return executeRequest(request, callBack);
}private Request createRequest(String url, MultipartBody multipartBody, RequestParams mToken, long start) {Headers.Builder mHeadersBuilder = new Headers.Builder();for (Map.Entry<String, String> entry : mToken.urlParams.entrySet()) {mHeadersBuilder.add(entry.getKey(), entry.getValue());}Request.Builder requestBuilder = new Request.Builder().url(url).headers(mHeadersBuilder.build()).post(multipartBody);return requestBuilder.build();
}private boolean executeRequest(Request request, LoadTasksCallBack callBack) {try (Response response = client.newCall(request).execute()) {if (response.isSuccessful()) {String string = response.body().string();System.out.println("FileonResponse: " + string);callBack.onSuccess(string);return true;} else if (response.code() == 416) {// 416表示请求的Range无效,可能需要重新上传该分块System.out.println("onResponse: 分块上传失败,重新上传该分块");callBack.onFailed("分块上传失败,重新上传该分块");return false;} else {System.out.println("onResponse: 上传失败,状态码: " + response.code());callBack.onFailed("上传失败,状态码: " + response.code());return false;}} catch (IOException e) {System.out.println("onFailure: " + "上传失败");e.printStackTrace();callBack.onFailed("上传失败: " + e.getMessage());return false;}
}

分块相关

为视频文件的指定分块生成一个RequestBody对象,用于通过OkHttp将分块数据流式上传到服务器,避免一次性加载大文件到内存。

private RequestBody createChunkRequestBody(File videoFile,int chunkIndex, long start, long end) {return new RequestBody() {@Overridepublic MediaType contentType() {return MediaType.parse("video/mp4");}@Overridepublic void writeTo(BufferedSink sink) throws IOException {try (RandomAccessFile file = new RandomAccessFile(videoFile, "r");FileChannel channel = file.getChannel()) {ByteBuffer buffer = ByteBuffer.allocate(8192);long position = start;long remaining = end - start;while (remaining > 0) {int readSize = (int) Math.min(buffer.capacity(), remaining);buffer.limit(readSize);int bytesRead = channel.read(buffer, position);if (bytesRead == -1) break;sink.write(buffer.array(), 0, bytesRead);position += bytesRead;remaining -= bytesRead;buffer.clear();}}}};
}

方法签名

private RequestBody createChunkRequestBody(File videoFile,      // 要上传的视频文件int chunkIndex,      // 当前分块的索引(未直接使用)long start,          // 分块起始字节位置long end             // 分块结束字节位置
) 

匿名内部类

返回一个自定义的RequestBody对象,重写两个关键方法:

contentType() - 指定内容类型
@Override
public MediaType contentType() {return MediaType.parse("video/mp4"); // 明确告知服务器上传的是MP4视频
}
writeTo(BufferedSink sink) - 数据写入逻辑
@Override
public void writeTo(BufferedSink sink) throws IOException {try (RandomAccessFile file = new RandomAccessFile(videoFile, "r"); // 只读模式打开文件FileChannel channel = file.getChannel()                       // 获取NIO文件通道) {ByteBuffer buffer = ByteBuffer.allocate(8192); // 分配8KB缓冲区long position = start;    // 当前读取位置long remaining = end - start; // 剩余需读取的字节数while (remaining > 0) {// 确定本次读取的字节数int readSize = (int) Math.min(buffer.capacity(), remaining);buffer.limit(readSize); // 设置缓冲区读取上限// 从文件指定位置读取数据到缓冲区int bytesRead = channel.read(buffer, position);if (bytesRead == -1) break; // 文件已读完// 将缓冲区数据写入网络流sink.write(buffer.array(), 0, bytesRead);// 更新位置和剩余字节数position += bytesRead;remaining -= bytesRead;buffer.clear(); // 重置缓冲区供下次使用}}
}

流程示意

+----------------+         +----------------+         +----------------+
|   视频文件      |         |   ByteBuffer    |         |  OkHttp请求流   |
| (分块范围:      | ---->   | (8KB缓冲区)     | ---->   | (BufferedSink)  |
|  start - end)  |         +----------------+         +----------------+
+----------------+| 每次定位到| 新的position+-----------------+

相关文章:

  • vue注册用户使用v-model实现数据双向绑定
  • Kotlin 协程 vs RxJava vs 线程池:性能与场景对比
  • Spring boot 简单开发接口
  • 超详细fish-speech本地部署教程
  • LLaVA:开源多模态大语言模型深度解析
  • 数据结构中的栈与队列:原理、实现与应用
  • C++GO语言微服务和服务发现②
  • 【Bootstrap V4系列】学习入门教程之 组件-表单(Forms)高级用法(二)
  • Java数据结构——二叉树
  • 封装 RabbitMQ 消息代理交互的功能
  • 【C++ Qt】容器类(GroupBox、TabWidget)内附思维导图 通俗易懂
  • 【算法-哈希表】常见算法题的哈希表套路拆解
  • 【Linux系列】跨平台安装与配置 Vim 文本编辑器
  • SierraNet协议分析使用指导[RDMA]| 如何设置 NVMe QP 端口以进行正确解码
  • Eclipse 插件开发 6 右键菜单
  • Web自动化测试入门详解
  • 街景主观感知全流程(自建数据集+两两对比程序+Trueskill计算评分代码+训练模型+大规模预测)27
  • 使用谱聚类将相似度矩阵分为2类
  • OpenAI的商业化之路:从非营利到盈利的转型
  • 【金仓数据库征文】金仓数据库KingbaseES: 技术优势与实践指南(包含安装)
  • 上海“电子支付费率成本为0”背后:金融服务不仅“快”和“省”,更有“稳”和“准”
  • 眉山“笑气”迷局:草莓熊瓶背后的隐秘与危机
  • 上海“随申兑”服务平台有哪些功能?已归集800余个惠企政策
  • 陕西澄城打造“中国樱桃第一县”:从黄土高原走向海外,年产值超30亿
  • 央行:5月15日起下调金融机构存款准备金率0.5个百分点
  • 刘诚宇、杨皓宇进球背后,是申花本土球员带着外援踢的无奈