Skip to content

大文件下载、断点续传与多线程分片下载架构 ​

回到总览:异步后台任务、保活、系统能力
相关模块:WorkManager / ForegroundService / AlarmManager 对比 · 文件与媒体上传管线:直传 OSS、压缩与分片断点续传

一句话定义 ​

大文件下载与断点续传架构是指在移动网络不稳定、网络切换或 App 退后台/进程被杀场景下,利用 HTTP 的 Range 报头、RandomAccessFile 本地随机读写、MD5 完整性校验以及后台任务组件(WorkManager / FGS),实现高可靠、可恢复、支持多线程分片的大文件传输工程方案。

代码索引 ​

计划补齐的实验:

  • labs/android/host/topics/background-tasks/download-manager-demo/ — HTTP Range 断点续传、RandomAccessFile 写入与 MD5 校验测试

为什么需要 ​

  • 为什么下载几百兆的资源包(如游戏 Asset、App 热更新包)不能像常规 API 那样一次性读取入内存处理?
    • 一句话答:一次性读入大文件会直接触发 OOM;必须使用流式读取(Streaming)逐块写入磁盘,并且必须支持断点续传,否则用户在下载到 99% 时网络瞬断就会导致前功尽弃,造成流量浪费与极差体验。
  • 为什么 10 年 Android 工程师设计下载器时必须处理 Range 报头与 ETag 校验?
    • 一句话答:HTTP Range 允许只请求文件的某一段字节区间(如 bytes=5000-);但如果在下载过程中服务端文件被更新了,不校验 ETag 或 Last-Modified 会导致拼接新旧不同文件的数据块,下载文件损坏。

底层机制 ​

1. HTTP 断点续传三要素 ​

text
[Client 下载发起] ── Range: bytes=1024- ──► [Server 服务器]
                  ◄── 206 Partial Content ─── (返回 Content-Range & ETag)
                              │
                    [RandomAccessFile] ── (seek 到 1024 字节追加写入)
  1. 请求头请求区间:Range: bytes=startPos-(如 Range: bytes=2048-5096)。
  2. 响应头校验与状态码:服务端如果支持断点续传,返回 206 Partial Content 状态码,并在 Header 中带上 Content-Range: bytes 2048-5096/10240 和 ETag。
  3. 本地随机写入:客户端使用 RandomAccessFile.seek(startPos) 将读到的字节流直接写入指定偏移位置。

2. 多线程分片并行下载架构 (Multi-thread Chunk Download) ​

对于超大文件,可将其切割为 N 个固定 Chunk(如每个 10MB):

text
文件总大小: 30MB
线程 1 ─── Range: bytes=0-10485759 ─────────► 写入 Segment 1
线程 2 ─── Range: bytes=10485760-20971519 ──► 写入 Segment 2
线程 3 ─── Range: bytes=20971520-31457279 ──► 写入 Segment 3
                                                │
                                                ▼ (下载完成,合并文件 & MD5 校验)

Android / Flutter / Web / Backend 对照 ​

维度Android 宿主Flutter (Dart)iOS 宿主
系统下载器DownloadManager (适合公网大文件)调原生或 flutter_downloaderNSURLSessionDownloadTask
断点写入文件RandomAccessFile / FileChannelRandomAccessFile (dart:io)NSFileHandle
后台持续WorkManager (前台 FGS 加持)结合原生后台 TaskNSURLSession 后台传输

常见场景 ​

1. 使用 HTTP Range 实现断点续传关键代码 ​

java
URL url = new URL(downloadUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
// 1. 设置 Range 报头,指定从已下载字节位置开始
long existingLength = destinationFile.length();
conn.setRequestProperty("Range", "bytes=" + existingLength + "-");

if (conn.getResponseCode() == HttpURLConnection.HTTP_PARTIAL) { // 206
    InputStream in = conn.getInputStream();
    RandomAccessFile raf = new RandomAccessFile(destinationFile, "rw");
    raf.seek(existingLength); // 2. 移动指针到已有文件末尾

    byte[] buffer = new byte[8192];
    int len;
    while ((len = in.read(buffer)) != -1) {
        raf.write(buffer, 0, len); // 3. 逐块流式写入,严禁一次性读入内存
    }
    raf.close();
}

常见误配、事故后果与排障 ​

1. 事故:服务端文件已更新,客户端按旧 Range 续传导致文件损坏 ​

  • 误配原因:客户端仅根据本地已有文件长度发 Range: bytes=1000-,没有保存和比对上一次响应的 ETag。
  • 后果:服务端在该期间重新发布了新版本文件,拼接出来的是“前半段旧版本 + 后半段新版本”的混合垃圾文件,校验 MD5 报错或解压失败。
  • 排障与修法:在数据库中记录上一次下载时的 ETag / Last-Modified;发请求时同时带上 If-Range: "etag-value"。若服务端 ETag 已变动,服务器会返回 200 OK 并重新下发完整新文件。

与相近概念对比 ​

下载方案是否支持断点续传是否支持后台被杀恢复适合场景
系统 DownloadManager是是 (系统级服务管理)APK 更新包、公开媒体资源下载
OkHttp + RandomAccessFile是依赖应用自身 FGS / WorkManager自定义业务数据包、需要自定义 Token 鉴权
普通 OkHttp Stream否否几百 KB ~ 数 MB 的小文件下载

对应实验 ​

计划补齐的实验:

  • labs/android/host/topics/background-tasks/download-manager-demo/ — HTTP Range 断点续传、RandomAccessFile 写入与 MD5 校验测试

复习检查题 ​

  1. HTTP 断点续传中,响应状态码 200 OK 与 206 Partial Content 有何区别?

    答:200 OK 表示服务器忽略了客户端发出的 Range 请求报头(或服务器不支持断点续传),正将完整的原始文件从第 0 字节开始重新下发;而 206 Partial Content 表示服务器成功识别了 Range 报头,当前仅返回客户端所请求的那一段字节区间,客户端可进行追加写入。

  2. 在大文件下载完成后,为什么必须进行 MD5 或 SHA-256 校验?

    答:由于网络传输过程中可能发生数据丢包、TCP 坏块重传错误、代理服务器缓存污染,或者断点续传时拼接了错误的文件片段,文件虽然长度一致但内容可能已损坏。通过对下载完成的文件计算 MD5 并与服务器给出的哈希值进行比对,能 100% 确保文件完整性与安全性。

速记 ​

  • 断点三要件:HTTP Range 报头、206 Partial 状态码与 RandomAccessFile 随机写入。
  • 防坏块防护:带上 If-Range 校验 ETag,避免拼接新旧不同文件导致文件损坏。
  • 内存防爆:流式 Buffer 逐块落盘,下载完成必校验 MD5。

站点构建时间:2026/8/24 23:43:17