首先介绍下这个Demo:点击开始下载后,开始下载一张图片;可以暂停,然后继续下载;上面可以显示下载进度;下载完成后,把下面的图片替换成我们下载的图片。
为了实现下载功能,这里用到的一个主要类URLSession
,下面首先介绍一下这个类。
URLSession
URLSession
类原生支持data
、file
、ftp
、http
和https
协议。跟它比较相关的另外一个类是URLSessionConfiguration
,这个类可以用来设置会话的相关配置,在初始化会话实例时,有可能会用到。
URLSession
类的层次结构
URLSession
类的层次结构如下:
-
URLSession
:一个会话对象 -
URLSessionConfiguration
:一个会话配置对象,用户初始化会话 -
URLSessionTask
:一个会话任务基类,下面三个是它的子类-
URLSessionDataTask
:获取URL内容并把获取到的内容作为Data
的任务-
URLSessionUploadTask
:URLSessionDataTask
的子类,一个上传文件的任务
-
-
URLSessionDownloadTask
:获取URL内容并把获取到的内容存储到ROM的任务 -
URLSessionStreamTask
:用于建立TCP/IP连接的任务
-
URLSession
API还提供了以下代理协议
-
URLSessionDelegate
:定义了处理会话级别的代理方法 -
URLSessionTaskDelegate
:定义了处理任务级别的代理方法 -
URLSessionDataDelegate
:定义了处理数据和上传任务级别的代理方法 -
URLSessionStreamDelegate
:定义了处理流任务级别的代理方法
初始化URLSession
实例
在初始化一个URLSession
的实例时,可以有四种情况,每一种情况处理不同的请求:
-
URLSession
有一个单例shared
,这个单例没有设置会话的相关配置,可以用于基本请求。 - 使用
URLSession
的初始化器,传入会话配置对象URLSessionConfiguration.default
。通过这种方式初始化的实例,类似于单例,但是这种方式可以设置一个代理,然后获取相关数据。这种方式会把缓存保存到ROM中、把证书保存到用户的钥匙链,同时也会保存cookie。 - 使用
URLSession
的初始化器,传入会话配置对象URLSessionConfiguration. ephemeral
,通过这种方式初始化的实例,类似于第二种创建方式,但是这种方式不会把缓存、证书和其他与会话相关的数据保存到ROM中,而是保存在RAM中,当会话失效后,这些数据会被自动清除。除非你自己手动保存到文件中。这种模式的优点主要是能保护用户的隐私。 - 使用
URLSession
的初始化器,传入会话配置对象URLSessionConfiguration.background(withIdentifier: "download")
,通过这种方式初始化的实例,可以支持HTTP
和HTTPS
在后台上传和下载文件。如果应用被系统杀死或者重新启动,应用可以使用同一个identifier
来创建一个新的会话配置对象,并获取被系统杀死时的传输状态。如果是被用户手动终止程序,那么系统会取消所有这个会话控制的后台传输,而且系统不会自动重新打开程序。只有用户手动重新启动应用,然后继续下载任务。让应用支持后台上传和下载,还需要把会话配置对象的属性isDiscretionary
设置为true
。
URLSession
的使用步骤
- 根据实际情况,创建一个会话配置
URLSessionConfiguration
对象 - 创建会话
URLSession
对象 - 使用会话对象创建一个任务
- 调用任务的
resume()
方法,开始执行任务
任务开始之后,会话将会调用以下代理方法:
- 最初与服务器握手需要一个连接认证,例如SSL客户端证书,会话调用
urlSession(_:task:didReceive:completionHandler:)
或者urlSession(_:didReceive:completionHandler:)
代理方法。 - 如果任务数据使用一个流提供的,调用
urlSession(_:task:needNewBodyStream:)
方法来获取一个InputStream
对象,以为新的请求提供数据。 - 在最初传数据给服务器时,代理将会周期性地收到
urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)
回调,在这个回调中,描述了上传过程。 - 服务器发回响应
- 如果响应里面要求需要认证,会话调用
urlSession(_:task:didReceive:completionHandler:)
。回到第二步。 - 如果响应是一个HTTP回调响应,会话调用
urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:)
。在这个代理方法里面,使用已提供的URLRequest
对象、一个新的URLRequest
对象(用于回调一个不同的URL)或者nil
(把回调的响应体最为一个有效的响应并且把它返回最为结果)来执行completionHandler
。
- 如果使用了这个回调,回到第二步。
- 如果代理没有实现这个方法,回调会跟踪回调的最大数量。
- 对于使用
downloadTask(withResumeData:)
或者downloadTask(withResumeData:completionHandler:)
创建的下载任务,会调用urlSession(_:downloadTask:didResumeAtOffset:expectedTotalBytes:)
。 - 对于一个数据任务,会调用
urlSession(_:dataTask:didReceive:completionHandler:)
。决定是否把数据任务转换为下载任务,然后调用completionHandler
来继续接收数据或者下载数据。
- 如果选择把数据任务转换为下载任务,会话会调用
urlSession(_:dataTask:didBecome:)
,调用这个方法之后,代理不会在从数据任务收到回调,而是收到下载任务回调。
- 在从服务器传输过程中,代理会周期性的收到任务级别的回调来报告传输的过程。
- 对于数据任务,会话调用
urlSession(_:dataTask:didReceive:)
。 - 对于下载任务,会话调用
urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:)
。使用任务的cancel(byProducingResumeData:)
方法来取消下载。通过任务的downloadTask(withResumeData:)
和downloadTask(withResumeData:completionHandler:)
来开启一个新的下载任务继续下载,回到第一步。
- 对于一个数据任务,会话可能会调用
urlSession(_:dataTask:willCacheResponse:completionHandler:)
。在这里,你要决定是否允许缓存。如果没有实现这个方法,那么将会默认使用会话配置对象指定的缓存策略来执行。 - 如果响应是多重编码的,会话可能会调用多次
didReceiveResponse
方法。 - 如果一个下载任务完成,会话调用
urlSession(_:downloadTask:didFinishDownloadingTo:)
,这个方法中有一个文件临时存放的位置。我们需要在这里使用这些数据或者把数据保存到一个永久的位置。 - 当任务完成时,会话调用
urlSession(_:task:didCompleteWithError:)
,这个方法中有一个可选的error
,如果error
为nil
,那么意味着文件已经下载完成。如果一个下载任务被用户暂停,下载任务可以继续,并且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地址 >>
如果文中有错误,请指出!我们共同学习,共同进步。谢谢!