为何URLSession.AsyncBytes的Task取消后下载仍持续?
问题:async/await 下 URLSession.bytes(from:) 无法有效取消下载任务?
核心原因
你遇到的问题本质是:URLSession.shared.bytes(from:) 返回的 AsyncBytes 与外层 Task 的取消机制没有强绑定。
当你取消外层 Task 时,AsyncBytes 的 for-await-in 循环不会立刻终止——它会先把已经缓存到本地缓冲区的字节全部处理完毕,才会响应取消信号抛出 CancellationError。更关键的是,底层的URLSession下载任务不会因为外层Task取消而自动停止,还会继续接收数据,直到缓冲区填满或者任务本身完成。
可行解决方案
方案1:手动绑定 Task 取消与 URLSessionTask 取消
放弃直接使用 shared 的 bytes(from:),手动创建 URLSessionDataTask,并在外层 Task 取消时主动调用任务的 cancel() 方法,同时跟踪字节传输进度:
func downloadWithCancelControl(url: URL) async throws -> (progress: AsyncStream<Int64>, cancel: () -> Void) { let session = URLSession.shared var dataTask: URLSessionDataTask? var totalReceivedBytes: Int64 = 0 // 创建用于传递进度的AsyncStream let progressStream = AsyncStream<Int64> { continuation in dataTask = session.dataTask(with: url) { data, _, error in if let error = error { continuation.finish(throwing: error) return } continuation.finish() } dataTask?.resume() // 监听Task取消 Task { await Task.sleep(UInt64(0.1 * 1_000_000_000)) // 短暂延迟确保任务启动 await Task.currentTask!.cancellation() dataTask?.cancel() continuation.finish() } } // 补充:如需实时跟踪字节,可结合URLSessionDelegate的data接收方法累加字节数并发送到stream let cancel = { dataTask?.cancel() } return (progressStream, cancel) }
方案2:使用 async/await 版 URLSessionDownloadTask
苹果提供了原生async版的下载任务API,URLSession.shared.download(from:),它的取消机制与外层Task强绑定,取消后会立刻终止底层下载。同时可以通过自定义URLSessionDownloadDelegate跟踪已传输字节数:
class DownloadProgressDelegate: NSObject, URLSessionDownloadDelegate { var updateProgress: ((Int64, Int64) -> Void)? func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { updateProgress?(totalBytesWritten, totalBytesExpectedToWrite) } } func startTrackableDownload(url: URL) async throws -> (savedURL: URL, cancel: () -> Void) { let progressDelegate = DownloadProgressDelegate() let session = URLSession(configuration: .default, delegate: progressDelegate, delegateQueue: .global()) // 实时打印进度 progressDelegate.updateProgress = { written, total in print("已传输:\(written)/\(total) 字节") } let downloadTask = Task { let (tempURL, response) = try await session.download(from: url) // 将临时文件移动到持久化目录 let destURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] .appendingPathComponent(response.suggestedFilename ?? "downloaded_file") try? FileManager.default.removeItem(at: destURL) try FileManager.default.moveItem(at: tempURL, to: destURL) return destURL } // 取消操作 let cancelAction = { downloadTask.cancel() session.invalidateAndCancel() } // 监听取消,确保session资源清理 Task { await downloadTask.cancellation() session.invalidateAndCancel() } let resultURL = try await downloadTask.value return (resultURL, cancelAction) }
总结
URLSession.shared.bytes(from:) 更适合处理小文件或不需要精确取消控制的场景,因为它的取消响应存在延迟且无法直接干预底层任务。如果需要严格的取消控制和进度跟踪,优先选择手动绑定任务取消,或者使用async版的downloadTask结合自定义delegate方案。
内容的提问来源于stack exchange,提问作者Thecafremo
相关产品推荐
相关产品推荐

