向后移植 URLSession 的下载(for:delegate:) 以供并发使用



我试图对URLSession的download(for:delegate:)进行后台移植,因为它需要iOS 15+的部署目标,但iOS 13+现在支持并发。

这似乎是一个非常直接的过程,因为您可以将延续传递到downloadTask(with:completionHandler:)的完成处理程序中。

这就是我想到的:

extension URLSession {
func asyncDownload(for request: URLRequest) async throws -> (URL, URLResponse) {
return try await withCheckedThrowingContinuation { continuation in
downloadTask(with: request) { url, response, error in
if let url = url, let response = response {
continuation.resume(returning: (url, response))
} else {
continuation.resume(throwing: error ?? URLError(.badServerResponse))
}
}.resume()
}
}
}

然而,downloadTask(with:completionHandler:)的实现中有一个微妙之处,它使代码无法正常工作。URL后面的文件在返回完成块后立即被删除。因此,该文件在等待之后不再可用。

我可以在继续之前放置FileManager.default.fileExists(atPath: url.path),并在等待此调用之后放置同一行,从而复制这一点。第一个产生了一个true,第二个产生了false

然后,我尝试使用苹果的实现download(for:delegate:),它神奇地没有出现我所描述的问题。给定URL处的文件在等待后仍然可用。

一个可能的解决方案是将文件移动到downloadTask的闭包中的另一个位置。然而,在我看来,这是一场关注分离的噩梦与关注分离的原则相冲突。我必须介绍FileManager对该调用的依赖性,苹果(可能(也没有这样做,因为downloadTask(with:completionHandler:)download(for:delegate:)返回的URL看起来完全相同。

所以我想知道是否有比我现在做的更好的解决方案来包装这个调用并使其异步。也许你可以以某种方式阻止闭包返回,直到Task完成?我希望将文件移动到更好的目的地的责任留给asyncDownload(for:)的调用方。

你说:

一个可能的解决方案是将文件移动到downloadTask闭包中的另一个位置。

是的,这正是你应该做的。你有一种处理本地文件系统中文件的方法,使用Foundation的FileManager并不是严重违反关注点分离的行为。此外,这是唯一合乎逻辑的选择。


FWIW,下面,我使用withCheckedThrowingContinuation,但我也使用withTaskCancellationHandler:取消它

extension URLSession {
@available(iOS, deprecated: 15, message: "Use `download(from:delegate:)` instead")
func download(with url: URL) async throws -> (URL, URLResponse) {
try await download(with: URLRequest(url: url))
}
@available(iOS, deprecated: 15, message: "Use `download(for:delegate:)` instead")
func download(with request: URLRequest) async throws -> (URL, URLResponse) {
let sessionTask = URLSessionTaskActor()
return try await withTaskCancellationHandler {
Task { await sessionTask.cancel() }
} operation: {
try await withCheckedThrowingContinuation { continuation in
Task {
await sessionTask.start(downloadTask(with: request) { location, response, error in
guard let location = location, let response = response else {
continuation.resume(throwing: error ?? URLError(.badServerResponse))
return
}
// since continuation can happen later, let's figure out where to store it ...
let tempURL = URL(fileURLWithPath: NSTemporaryDirectory())
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension(request.url!.pathExtension)
// ... and move it to there
do {
try FileManager.default.moveItem(at: location, to: tempURL)
continuation.resume(returning: (tempURL, response))
} catch {
continuation.resume(throwing: error)
}
})
}
}
}
}
}
private extension URLSession {
actor URLSessionTaskActor {
weak var task: URLSessionTask?
func start(_ task: URLSessionTask) {
self.task = task
task.resume()
}
func cancel() {
task?.cancel()
}
}
}

一旦验证了实现的正确性,就可以考虑使用withUnsafeThrowingContinuation而不是withCheckedThrowingContinuation

最新更新