Appearance
大文件下载、断点续传与多线程分片下载架构
回到总览:异步后台任务、保活、系统能力
相关模块: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会导致拼接新旧不同文件的数据块,下载文件损坏。
- 一句话答:HTTP
底层机制
1. HTTP 断点续传三要素
text
[Client 下载发起] ── Range: bytes=1024- ──► [Server 服务器]
◄── 206 Partial Content ─── (返回 Content-Range & ETag)
│
[RandomAccessFile] ── (seek 到 1024 字节追加写入)- 请求头请求区间:
Range: bytes=startPos-(如Range: bytes=2048-5096)。 - 响应头校验与状态码:服务端如果支持断点续传,返回
206 Partial Content状态码,并在 Header 中带上Content-Range: bytes 2048-5096/10240和ETag。 - 本地随机写入:客户端使用
RandomAccessFile.seek(startPos)将读到的字节流直接写入指定偏移位置。
2. 多线程分片并行下载架构 (Multi-thread Chunk Download)
对于超大文件,可将其切割为
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_downloader | NSURLSessionDownloadTask |
| 断点写入文件 | RandomAccessFile / FileChannel | RandomAccessFile (dart:io) | NSFileHandle |
| 后台持续 | WorkManager (前台 FGS 加持) | 结合原生后台 Task | NSURLSession 后台传输 |
常见场景
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 校验测试
复习检查题
HTTP 断点续传中,响应状态码
200 OK与206 Partial Content有何区别?答:
200 OK表示服务器忽略了客户端发出的Range请求报头(或服务器不支持断点续传),正将完整的原始文件从第 0 字节开始重新下发;而206 Partial Content表示服务器成功识别了Range报头,当前仅返回客户端所请求的那一段字节区间,客户端可进行追加写入。在大文件下载完成后,为什么必须进行 MD5 或 SHA-256 校验?
答:由于网络传输过程中可能发生数据丢包、TCP 坏块重传错误、代理服务器缓存污染,或者断点续传时拼接了错误的文件片段,文件虽然长度一致但内容可能已损坏。通过对下载完成的文件计算 MD5 并与服务器给出的哈希值进行比对,能 100% 确保文件完整性与安全性。
速记
- 断点三要件:HTTP
Range报头、206 Partial状态码与RandomAccessFile随机写入。 - 防坏块防护:带上
If-Range校验 ETag,避免拼接新旧不同文件导致文件损坏。 - 内存防爆:流式 Buffer 逐块落盘,下载完成必校验 MD5。