URLSession downloadTask 比互联网连接慢得多
Posted
技术标签:
【中文标题】URLSession downloadTask 比互联网连接慢得多【英文标题】:URLSession downloadTask much slower than internet connection 【发布时间】:2017-01-09 11:53:02 【问题描述】:我正在使用 URLSession 和 downloadTask 在前台下载文件。下载速度比预期慢得多。我发现的其他帖子解决了后台任务的问题。
let config = URLSessionConfiguration.default
config.httpMaximumConnectionsPerHost = 20
let session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
let request = URLRequest(url: url)
let completion: ((URL?, Error?) -> Void) = (tempLocalUrl, error) in
print("Download over")
value.completion = completion
value.task = self.session.downloadTask(with: request)
我观察到网络使用量约为 150kb/s,而我的设备上的速度测试报告连接速度为 5MB/s
=== 编辑
我可以确认,编写多部分下载的代码(这有点痛苦)可以大大加快速度。
【问题讨论】:
如果我一次开始多次下载,网络使用率会上升到 ~800kb/s,这只是我需要的一半 您能提供您的示例网址吗? 当然。我正在下载视频。一个例子可以是:s3.amazonaws.com/mettavr/videos/… (8.5 Mb) 你是如何测量速度的? 很难保持完全一致的带宽,因此不同设置的测试不会在完全相同的条件下运行。然而,他们预计不会有太大的变化。我一直在粗略地测量时间以及查看 Xcode 报告的网络使用情况。对于这两个指标,多部分下载的差异约为 10 倍。所以我认为这没有留下不准确结论的余地。 【参考方案1】:如果这对任何人有帮助,这是我加快下载速度的代码。它将文件下载拆分为多个文件部分下载,从而更有效地使用可用带宽。不得不这样做还是觉得不对……
最后的用法是这样的:
// task.pause is not implemented yet
let task = FileDownloadManager.shared.download(from:someUrl)
task.delegate = self
task.resume()
这是代码:
/// Holds a weak reverence
class Weak<T: AnyObject>
weak var value : T?
init (value: T)
self.value = value
enum DownloadError: Error
case missingData
/// Represents the download of one part of the file
fileprivate class DownloadTask
/// The position (included) of the first byte
let startOffset: Int64
/// The position (not included) of the last byte
let endOffset: Int64
/// The byte length of the part
var size: Int64 return endOffset - startOffset
/// The number of bytes currently written
var bytesWritten: Int64 = 0
/// The URL task corresponding to the download
let request: URLSessionDownloadTask
/// The disk location of the saved file
var didWriteTo: URL?
init(for url: URL, from start: Int64, to end: Int64, in session: URLSession)
startOffset = start
endOffset = end
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.allHTTPHeaderFields?["Range"] = "bytes=\(start)-\(end - 1)"
self.request = session.downloadTask(with: request)
/// Represents the download of a file (that is done in multi parts)
class MultiPartsDownloadTask
weak var delegate: MultiPartDownloadTaskDelegate?
/// the current progress, from 0 to 1
var progress: CGFloat
var total: Int64 = 0
var written: Int64 = 0
parts.forEach( part in
total += part.size
written += part.bytesWritten
)
guard total > 0 else return 0
return CGFloat(written) / CGFloat(total)
fileprivate var parts = [DownloadTask]()
fileprivate var contentLength: Int64?
fileprivate let url: URL
private var session: URLSession
private var isStoped = false
private var isResumed = false
/// When the download started
private var startedAt: Date
/// An estimate on how long left before the download is over
var remainingTimeEstimate: CGFloat
let progress = self.progress
guard progress > 0 else return CGFloat.greatestFiniteMagnitude
return CGFloat(Date().timeIntervalSince(startedAt)) / progress * (1 - progress)
fileprivate init(from url: URL, in session: URLSession)
self.url = url
self.session = session
startedAt = Date()
getRemoteResourceSize().then [weak self] size -> Void in
guard let wself = self else return
wself.contentLength = size
wself.createDownloadParts()
if wself.isResumed
wself.resume()
.catch [weak self] error in
guard let wself = self else return
wself.isStoped = true
/// Start the download
func resume()
guard !isStoped else return
startedAt = Date()
isResumed = true
parts.forEach( $0.request.resume() )
/// Cancels the download
func cancel()
guard !isStoped else return
parts.forEach( $0.request.cancel() )
/// Fetch the file size of a remote resource
private func getRemoteResourceSize(completion: @escaping (Int64?, Error?) -> Void)
var headRequest = URLRequest(url: url)
headRequest.httpMethod = "HEAD"
session.dataTask(with: headRequest, completionHandler: (data, response, error) in
if let error = error
completion(nil, error)
return
guard let expectedContentLength = response?.expectedContentLength else
completion(nil, FileCacheError.sizeNotAvailableForRemoteResource)
return
completion(expectedContentLength, nil)
).resume()
/// Split the download request into multiple request to use more bandwidth
private func createDownloadParts()
guard let size = contentLength else return
let numberOfRequests = 20
for i in 0..<numberOfRequests
let start = Int64(ceil(CGFloat(Int64(i) * size) / CGFloat(numberOfRequests)))
let end = Int64(ceil(CGFloat(Int64(i + 1) * size) / CGFloat(numberOfRequests)))
parts.append(DownloadTask(for: url, from: start, to: end, in: session))
fileprivate func didFail(_ error: Error)
cancel()
delegate?.didFail(self, error: error)
fileprivate func didFinishOnePart()
if parts.filter( $0.didWriteTo != nil ).count == parts.count
mergeFiles()
/// Put together the download files
private func mergeFiles()
let ext = self.url.pathExtension
let destination = Constants.tempDirectory
.appendingPathComponent("\(String.random(ofLength: 5))")
.appendingPathExtension(ext)
do
let partLocations = parts.flatMap( $0.didWriteTo )
try FileManager.default.merge(files: partLocations, to: destination)
delegate?.didFinish(self, didFinishDownloadingTo: destination)
for partLocation in partLocations
do
try FileManager.default.removeItem(at: partLocation)
catch
report(error)
catch
delegate?.didFail(self, error: error)
deinit
FileDownloadManager.shared.tasks = FileDownloadManager.shared.tasks.filter(
$0.value !== self
)
protocol MultiPartDownloadTaskDelegate: class
/// Called when the download progress changed
func didProgress(
_ downloadTask: MultiPartsDownloadTask
)
/// Called when the download finished succesfully
func didFinish(
_ downloadTask: MultiPartsDownloadTask,
didFinishDownloadingTo location: URL
)
/// Called when the download failed
func didFail(_ downloadTask: MultiPartsDownloadTask, error: Error)
/// Manage files downloads
class FileDownloadManager: NSObject
static let shared = FileDownloadManager()
private var session: URLSession!
fileprivate var tasks = [Weak<MultiPartsDownloadTask>]()
private override init()
super.init()
let config = URLSessionConfiguration.default
config.httpMaximumConnectionsPerHost = 50
session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
/// Create a task to download a file
func download(from url: URL) -> MultiPartsDownloadTask
let task = MultiPartsDownloadTask(from: url, in: session)
tasks.append(Weak(value: task))
return task
/// Returns the download task that correspond to the URL task
fileprivate func match(request: URLSessionTask) -> (MultiPartsDownloadTask, DownloadTask)?
for wtask in tasks
if let task = wtask.value
for part in task.parts
if part.request == request
return (task, part)
return nil
extension FileDownloadManager: URLSessionDownloadDelegate
public func urlSession(
_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didWriteData bytesWritten: Int64,
totalBytesWritten: Int64,
totalBytesExpectedToWrite: Int64
)
guard let x = match(request: downloadTask) else return
let multiPart = x.0
let part = x.1
part.bytesWritten = totalBytesWritten
multiPart.delegate?.didProgress(multiPart)
func urlSession(
_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didFinishDownloadingTo location: URL
)
guard let x = match(request: downloadTask) else return
let multiPart = x.0
let part = x.1
let ext = multiPart.url.pathExtension
let destination = Constants.tempDirectory
.appendingPathComponent("\(String.random(ofLength: 5))")
.appendingPathExtension(ext)
do
try FileManager.default.moveItem(at: location, to: destination)
catch
multiPart.didFail(error)
return
part.didWriteTo = destination
multiPart.didFinishOnePart()
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?)
guard let error = error, let multipart = match(request: task)?.0 else return
multipart.didFail(error)
extension FileManager
/// Merge the files into one (without deleting the files)
func merge(files: [URL], to destination: URL, chunkSize: Int = 1000000) throws
FileManager.default.createFile(atPath: destination.path, contents: nil, attributes: nil)
let writer = try FileHandle(forWritingTo: destination)
try files.forEach( partLocation in
let reader = try FileHandle(forReadingFrom: partLocation)
var data = reader.readData(ofLength: chunkSize)
while data.count > 0
writer.write(data)
data = reader.readData(ofLength: chunkSize)
reader.closeFile()
)
writer.closeFile()
【讨论】:
以上是关于URLSession downloadTask 比互联网连接慢得多的主要内容,如果未能解决你的问题,请参考以下文章
URLSession 没有返回位置数据(session:downloadTask:didFinishDownloadingToURL 位置)
`urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:)` 在下载内容时只调用一次使用