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:)` 在下载内容时只调用一次使用

URLSession downloadTask 在后台运行时的行为?

URLSession - 下载远程目录

未调用 URLSession 委托成功方法,但没有错误

在 Swiftui 中,如何检查 URLsession 中的内容?