核心是手动发请求分块读流并计算百分比:先用GetAsync预读Content-Length,再ReadAsStreamAsync获取流,配合Progress更新UI,注意线程安全与缓冲区控制。

用 HttpClient + Progress<double></double> 拿到实时下载进度
核心是别用 DownloadFileAsync 这种黑盒方法——它不暴露进度流。得手动发请求、分块读响应流,再自己算百分比。
关键点在于:服务器必须返回 Content-Length 头,否则你根本不知道总大小,没法算百分比。遇到没有这个头的接口(比如某些 chunked 编码或动态生成文件),进度条只能显示“正在下载”,不能显示百分比。
- 用
HttpClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead)预读响应头,立刻拿到Content-Length - 调用
response.Content.ReadAsStreamAsync()获取原始流,避免被自动缓冲 - 用
Progress<double></double>回调更新 UI,值范围是 0–100(不是 0–1) - 别在 UI 线程里直接读流,要用
await stream.ReadAsync(buffer, cancellationToken)配合ConfigureAwait(false)
WebClient 的 DownloadProgressChanged 为什么经常不准
它底层靠的是已接收字节数除以响应头里的 Content-Length,但有两个致命坑:
- 如果服务器没发
Content-Length(比如用了Transfer-Encoding: chunked),事件里的e.TotalBytesToReceive就是 -1,百分比永远是 0 - 某些反向代理(Nginx、CDN)会改写或删掉
Content-Length,导致计算值远小于实际大小,进度条飞快冲到 100% 然后卡住 - 它不区分 TCP 接收完成和磁盘写入完成——文件真正落盘可能滞后,用户看到“100%”但文件还打不开
所以除非你完全控制服务端且确认头信息可靠,否则别依赖 WebClient 的进度事件做精确反馈。
UI 线程更新进度时最常见的线程崩法
进度回调默认在 ThreadPool 线程触发,而 WinForms/WPF/MAUI 的控件只能由创建它的线程访问。直接赋值 progressBar.Value = e.ProgressPercentage 会抛 InvalidOperationException。
- WinForms:用
this.Invoke((MethodInvoker)delegate { progressBar.Value = value; }) - WPF:用
Dispatcher.Invoke(() => progressBar.Value = value) - MAUI:用
MainThread.InvokeOnMainThreadAsync(() => progressBar.Progress = value / 100.0) - 更稳妥的做法是把
Progress<t></t>实例在 UI 线程构造,它会自动捕获同步上下文
大文件下载时内存暴涨或卡 UI 的真实原因
不是进度条的问题,是读流方式不对。常见错误是:一次性 ReadToEndAsync() 或用 MemoryStream 缓存整个文件内容。
- 正确做法是固定小缓冲区(如 8192 字节),循环
ReadAsync→WriteAsync到目标文件流,边下边写 - 记得给
FileStream加FileOptions.Asynchronous标志,否则异步写变成同步阻塞 - 别用
File.WriteAllBytes收尾——它会把全部数据加载进内存再写,1GB 文件直接 OOM - 如果还要校验哈希,别等下载完再算,边写边更新
HashAlgorithm.TransformBlock
进度条本身不耗资源,但背后的数据搬运逻辑一错,就变成内存泄漏+假死的组合拳。


















