【iOS开发】URLSession简介 & 大文件下载断点续传

首先介绍下这个Demo:点击开始下载后,开始下载一张图片;可以暂停,然后继续下载;上面可以显示下载进度;下载完成后,把下面的图片替换成我们下载的图片。

【iOS开发】URLSession简介 & 大文件下载断点续传_第1张图片

界面

为了实现下载功能,这里用到的一个主要类URLSession,下面首先介绍一下这个类。

URLSession

URLSession类原生支持datafileftphttphttps协议。跟它比较相关的另外一个类是URLSessionConfiguration,这个类可以用来设置会话的相关配置,在初始化会话实例时,有可能会用到。

URLSession类的层次结构

URLSession类的层次结构如下:

  • URLSession:一个会话对象
  • URLSessionConfiguration:一个会话配置对象,用户初始化会话
  • URLSessionTask:一个会话任务基类,下面三个是它的子类
    • URLSessionDataTask:获取URL内容并把获取到的内容作为Data的任务
      • URLSessionUploadTaskURLSessionDataTask的子类,一个上传文件的任务
    • URLSessionDownloadTask:获取URL内容并把获取到的内容存储到ROM的任务
    • URLSessionStreamTask:用于建立TCP/IP连接的任务

URLSessionAPI还提供了以下代理协议

  • URLSessionDelegate:定义了处理会话级别的代理方法
  • URLSessionTaskDelegate:定义了处理任务级别的代理方法
  • URLSessionDataDelegate:定义了处理数据和上传任务级别的代理方法
  • URLSessionStreamDelegate:定义了处理流任务级别的代理方法

初始化URLSession实例

在初始化一个URLSession的实例时,可以有四种情况,每一种情况处理不同的请求:

  • URLSession有一个单例shared,这个单例没有设置会话的相关配置,可以用于基本请求。
  • 使用URLSession的初始化器,传入会话配置对象URLSessionConfiguration.default。通过这种方式初始化的实例,类似于单例,但是这种方式可以设置一个代理,然后获取相关数据。这种方式会把缓存保存到ROM中、把证书保存到用户的钥匙链,同时也会保存cookie。
  • 使用URLSession的初始化器,传入会话配置对象URLSessionConfiguration. ephemeral,通过这种方式初始化的实例,类似于第二种创建方式,但是这种方式不会把缓存、证书和其他与会话相关的数据保存到ROM中,而是保存在RAM中,当会话失效后,这些数据会被自动清除。除非你自己手动保存到文件中。这种模式的优点主要是能保护用户的隐私。
  • 使用URLSession的初始化器,传入会话配置对象URLSessionConfiguration.background(withIdentifier: "download"),通过这种方式初始化的实例,可以支持HTTPHTTPS在后台上传和下载文件。如果应用被系统杀死或者重新启动,应用可以使用同一个identifier来创建一个新的会话配置对象,并获取被系统杀死时的传输状态。如果是被用户手动终止程序,那么系统会取消所有这个会话控制的后台传输,而且系统不会自动重新打开程序。只有用户手动重新启动应用,然后继续下载任务。让应用支持后台上传和下载,还需要把会话配置对象的属性isDiscretionary设置为true

URLSession的使用步骤

  • 根据实际情况,创建一个会话配置URLSessionConfiguration对象
  • 创建会话URLSession对象
  • 使用会话对象创建一个任务
  • 调用任务的resume()方法,开始执行任务

任务开始之后,会话将会调用以下代理方法:

  1. 最初与服务器握手需要一个连接认证,例如SSL客户端证书,会话调用urlSession(_:task:didReceive:completionHandler:)或者urlSession(_:didReceive:completionHandler:)代理方法。
  2. 如果任务数据使用一个流提供的,调用urlSession(_:task:needNewBodyStream:)方法来获取一个InputStream对象,以为新的请求提供数据。
  3. 在最初传数据给服务器时,代理将会周期性地收到urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)回调,在这个回调中,描述了上传过程。
  4. 服务器发回响应
  5. 如果响应里面要求需要认证,会话调用urlSession(_:task:didReceive:completionHandler:)。回到第二步。
  6. 如果响应是一个HTTP回调响应,会话调用urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:)。在这个代理方法里面,使用已提供的URLRequest对象、一个新的URLRequest对象(用于回调一个不同的URL)或者nil(把回调的响应体最为一个有效的响应并且把它返回最为结果)来执行completionHandler
  • 如果使用了这个回调,回到第二步。
  • 如果代理没有实现这个方法,回调会跟踪回调的最大数量。
  1. 对于使用downloadTask(withResumeData:)或者downloadTask(withResumeData:completionHandler:)创建的下载任务,会调用urlSession(_:downloadTask:didResumeAtOffset:expectedTotalBytes:)
  2. 对于一个数据任务,会调用urlSession(_:dataTask:didReceive:completionHandler:)。决定是否把数据任务转换为下载任务,然后调用completionHandler来继续接收数据或者下载数据。
  • 如果选择把数据任务转换为下载任务,会话会调用urlSession(_:dataTask:didBecome:),调用这个方法之后,代理不会在从数据任务收到回调,而是收到下载任务回调。
  1. 在从服务器传输过程中,代理会周期性的收到任务级别的回调来报告传输的过程。
  • 对于数据任务,会话调用urlSession(_:dataTask:didReceive:)
  • 对于下载任务,会话调用urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:)。使用任务的cancel(byProducingResumeData:)方法来取消下载。通过任务的downloadTask(withResumeData:)downloadTask(withResumeData:completionHandler:)来开启一个新的下载任务继续下载,回到第一步。
  1. 对于一个数据任务,会话可能会调用urlSession(_:dataTask:willCacheResponse:completionHandler:)。在这里,你要决定是否允许缓存。如果没有实现这个方法,那么将会默认使用会话配置对象指定的缓存策略来执行。
  2. 如果响应是多重编码的,会话可能会调用多次didReceiveResponse方法。
  3. 如果一个下载任务完成,会话调用urlSession(_:downloadTask:didFinishDownloadingTo:),这个方法中有一个文件临时存放的位置。我们需要在这里使用这些数据或者把数据保存到一个永久的位置。
  4. 当任务完成时,会话调用urlSession(_:task:didCompleteWithError:),这个方法中有一个可选的error,如果errornil,那么意味着文件已经下载完成。如果一个下载任务被用户暂停,下载任务可以继续,并且error不为nil,这个error中包含了一个userInfo字典,NSURLSessionDownloadTaskResumeData键对应的值就是目前已经下载的数据,我要要保存起来供继续下载使用。调用会话的downloadTask(withResumeData:)或者downloadTask(withResumeData:completionHandler:)方法新建一个下载任务,继续下载未完成的内容。如果这个任务不能继续,那么需要重新新建一个下载任务,重新下载。

代码演示

用一个结构来存储这个控制器要用到的常量:

private struct Constants {
    static let kDownload = "Download"
    static let kStartDownload = "开始下载"
    static let kPauseDownload = "暂停下载"
    static let kResumeDownload = "继续下载"
    static let kCompleteDownload = "下载完成"
}

点击下载按钮后,根据按钮的标题做相应的操作:

    @IBAction func startDownload(_ button: UIButton) {
        switch button.currentTitle! {
            
        case Constants.kStartDownload:
            currentTitle = Constants.kPauseDownload
            UIApplication.shared.isNetworkActivityIndicatorVisible = true
            
            // 创建会话相关配置
            let config = URLSessionConfiguration.background(withIdentifier: Constants.kDownload)
            // 在应用进入后台时,让系统决定决定是否在后台继续下载。如果是false,进入后台将暂停下载
            config.isDiscretionary = true
            
            // 创建一个可以在后台下载的session (其实会话的类型有四种形式)
            session = URLSession(configuration: config, delegate: self, delegateQueue: .main)
            task = session.downloadTask(with: request)
            task.resume()
            
        case Constants.kPauseDownload:
            UIApplication.shared.isNetworkActivityIndicatorVisible = false
            currentTitle = Constants.kResumeDownload
            
            // 保存已经下载的位置
            task.cancel { (data) in
                self.resumeData = data
            }
            
        case Constants.kResumeDownload:
            UIApplication.shared.isNetworkActivityIndicatorVisible = true
            currentTitle = Constants.kPauseDownload
            
            // 重新建立一个下载任务,继续下载未完成的数据
            task = session.downloadTask(withResumeData: resumeData)
            task.resume()
            
        case Constants.kCompleteDownload:
            print("kCompleteDownload-----下载完成")
            
        default:
            break
        }
    }

下面是实现URLSessionDownloadDelegate的代理方法:

extension ViewController: URLSessionDownloadDelegate {
    
    // 每下载完一部分调用,可能会调用多次
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
        progressView.progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
        
        print(progressView.progress)
    }
    
    // 下载完成后调用
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        // 下载完成后,保存到缓存目录
        let destination = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).last! + "/" + downloadTask.response!.suggestedFilename!
        do {
            try FileManager.default.moveItem(atPath: location.path, toPath: destination)
        }
        catch {
            print(error.localizedDescription)
        }
        
        UIApplication.shared.isNetworkActivityIndicatorVisible = false
        imageView.image = UIImage(contentsOfFile: destination)
        currentTitle = Constants.kCompleteDownload
        // 在后台下载完成,重新进入前台,把progress设置为1.0;如果不设置,progress的值是进入后台之前的值
        progressView.progress = 1.0
        session.invalidateAndCancel() // 下载完成,使session失效
    }
    
    // 任务完成时调用,但是不一定下载完成;用户点击暂停后,也会调用这个方法
    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        if error != nil {
            // 如果下载任务可以恢复,那么NSError的userInfo包含了NSURLSessionDownloadTaskResumeData键对应的数据,保存起来,继续下载要用到
            if let data = (error as! NSError).userInfo[NSURLSessionDownloadTaskResumeData] as? Data {
                resumeData =  data
            }
        }
    }
    
    // 继续下载时调用
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64) {
        print("didResumeAtOffset-----继续下载")
    }
}

关键代码都在这里,大家可以下载Demo。有什么问题,欢迎留言。谢谢!

Demo地址 >>

 



转载作者:Lebron_James
链接:https://www.jianshu.com/p/af3eb9501fe0
来源:简书

你可能感兴趣的:(【iOS开发】URLSession简介 & 大文件下载断点续传)