当前位置: 首页 > 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+-----------------+
http://www.xdnf.cn/news/371233.html

相关文章:

  • 【金仓数据库征文】学校AI数字人:从Sql Server到KingbaseES的数据库转型之路
  • 基于GF域的多进制QC-LDPC误码率matlab仿真,译码采用EMS算法
  • Spring之AOP
  • 信息检索(包含源码)
  • 服务预热原理
  • 动态路由EIGRP的配置
  • AutoGen+Deepseek+chainlit的简单使用
  • iOS瀑布流布局的实现(swift)
  • HNUST湖南科技大学-软件测试期中复习考点(保命版)
  • Kubernetes应用发布方式完整流程指南
  • Dia浏览器:AI驱动浏览网页,究竟怎么样?(含注册申请体验流程)
  • Harness: 全流程 DevOps 解决方案,让持续集成如吃饭般简单
  • 【字节拥抱开源】字节豆包团队开源首发 Seed-Coder 大模型
  • QSFP+、QSFP28、QSFP-DD接口分别实现40G、100G、200G/400G以太网接口
  • Flask 调试的时候进入main函数两次
  • 机器学习扫盲系列-深入浅出“反向传播”(二)
  • 第21天打卡
  • 流动式起重机Q2考试的实操部分,重点复习内容包括哪些方面?
  • 路由策略和策略路由的区别以及配置案例
  • 【C++指南】STL容器的安全革命:如何封装Vector杜绝越界访问与迭代器失效?
  • 图像处理篇---opencv实现坐姿检测
  • 系统级编程(四):利用windows API使用操作系统剪切板
  • [学习]RTKLib详解:rtksvr.c与streamsvr.c
  • Vue基础(8)_监视属性、深度监视、监视的简写形式
  • 扩容 QCOW2 磁盘镜像文件
  • 将循环队列中的各元素向右移动n步collections.deque.rotate(n)
  • 当可视化遇上 CesiumJS:突破传统,打造前沿生产配套方案
  • K8S服务的请求访问转发原理
  • Octave 绘图快速入门指南
  • jdk多版本切换,通过 maven 指定编译jdk版本不生效,解决思路