前言
这篇文章,咱们来说说Task
代理,通过之前的文章,我们可以知道一个普通的网络请求过程是:
- 根据一个
URL
和若干的参数来生成Request
。- 根据
Request
生成一个会话Session
。- 再根据这个
Session
生成Task
。- 我们开启
Task
就完成了这个请求。
在这个请求过程中,还有重定向、数据的上传、证书的验证、配置等信息。
其实,我们做iOS
开发的,对代理这个问题,不管是在网络请求中,还是用于代理回调等地方,我觉得代理就好比是一个拥有较高权限的管理员。这种方式在我们做业务开发中,是很好的处理方式。
URLSessionTask
Task分类
在苹果原生网络框架中,URLSessionTask
是最基础的task
任务封装,主要有以下几种task
:
URLSessionDataTask
URLSessionUploadTask
URLSessionDownloadTask
URLSessionStreamTask
注意:URLSessionStreamTask
这个我们先暂时不介绍,在后面的文章中单独介绍说明。
继承关系
我们先把URLSessionTask
的相关继承关系图给到大家看一下,具体代理方法我们后面慢慢讲。
URLSessionTask
URLSessionTask
子类继承关系:
URLSessionTaskDelegate
URLSessionTaskDelegate
继承自URLSessionDelegate
,URLSessionTaskDelegate
的主要协议方法有:
optional func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void)
optional func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
optional func urlSession(_ session: URLSession, task: URLSessionTask, needNewBodyStream completionHandler: @escaping (InputStream?) -> Void)
optional func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?)
optional func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64)
// 上传比较特殊一点,只有这一个跟上传相关的代理方法
URLSessionDataDelegate
URLSessionDataDelegate
则是继承自URLSessionTaskDelegate
协议,它的主要协议方法有:
optional func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void)
optional func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didBecome downloadTask: URLSessionDownloadTask)
optional func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data)
optional func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse, completionHandler: @escaping (CachedURLResponse?) -> Void)
URLSessionDownloadDelegate
URLSessionDownloadDelegate
同样是继承自URLSessionTaskDelegate
协议,主要方法有:
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL)
optional func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64)
optional func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64)
TaskDelegate
在Alamofire
中,咱们可以看到,TaskDelegate
类继承自NSObject
,处于继承链的最底层,它为我们提供了一些基础的属性,而且,这些属性是和其他的Delegate
共享使用:
// MARK: Properties
/// The serial operation queue used to execute all operations after the task completes.
public let queue: OperationQueue
/// The data returned by the server.
public var data: Data? { return nil }
/// The error generated throughout the lifecyle of the task.
public var error: Error?
var task: URLSessionTask? {
set {
taskLock.lock(); defer { taskLock.unlock() }
_task = newValue
}
get {
taskLock.lock(); defer { taskLock.unlock() }
return _task
}
}
var initialResponseTime: CFAbsoluteTime?
var credential: URLCredential?
var metrics: AnyObject? // URLSessionTaskMetrics
private var _task: URLSessionTask? {
didSet { reset() }
}
private let taskLock = NSLock()
我们接下来分析一下这些属性。
属性简介
queue
queue: OperationQueue
,显而易见,这就是一个队列,在队列中,我们可以添加很多的operation
,而且,当我们把isSuspended
的值设置为true
,就可以让队列中的所有operation
暂停,如果想要继续执行operation
,我们需要把isSuspended
的值设置为false
。
在Alamofire
框架中,有以下几种情况,会加入该队列的operation
:
- 队列在任务完成后,把
isSuspended
的值设置为false
。- 在任务完成后调用
Request
的endTime
,就可以为Request
设置请求结束时间。- 在处理
response
中,Alamofire
中的响应回调是链式的,原理就是把这些回调函数通过operation
添加到队列中,因此也保证了回调函数的访问顺序是正确的。- 还有上传数据成功后,删除一些临时文件等操作。
data
data: Data?
,服务器返回的Data,?
表示这个可能为空。
error
error: Error?
,这个应该很好理解了,在网络请求过程中,很有可能出现错误,那么我们添加这个属性,就是为了抓取过程中出现的错误。
task
task: URLSessionTask?
,就是表示一个task,很重要的属性。
initialResponseTime
initialResponseTime: CFAbsoluteTime?
,这是一个task
的响应时间,如果是URLSessionDataTask
,则表示接收到数据的时间;如果是URLSessionDownloadTask
,则表示开始写数据的时间;如果是URLSessionUploadTask
,则表示上传数据的时间。
credential
credential: URLCredential?
,这个表示证书,在做证书验证的时候用到。
metrics
metrics: AnyObject?
,这是苹果提供的一个统计task
信息的类URLSessionTaskMetrics
,它可以统计相关任务的事务,任务开始时间、结束时间,以及重定向的次数等信息。
生命周期Lifecycle
首先,还是来看一看代码:
// MARK: Lifecycle
init(task: URLSessionTask?) {
_task = task
self.queue = {
let operationQueue = OperationQueue()
operationQueue.maxConcurrentOperationCount = 1
operationQueue.isSuspended = true
operationQueue.qualityOfService = .utility
return operationQueue
}()
}
func reset() {
error = nil
initialResponseTime = nil
}
init函数
在初始化函数中,主要就是设置
operationQueue
,operationQueue.isSuspended = true
,就可以保证队列中的operation
都是暂停的,通常情况下,operation
在被加入到队列中后,会立即执行。
reset函数
reset
函数把error
和initialResponseTime
都置为nil
,这个就很简单了,不说了。
URLSessionTaskDelegate
接下来,我们看看URLSessionTaskDelegate
的相关函数:
// MARK: URLSessionTaskDelegate
var taskWillPerformHTTPRedirection: ((URLSession, URLSessionTask, HTTPURLResponse, URLRequest) -> URLRequest?)?
var taskDidReceiveChallenge: ((URLSession, URLSessionTask, URLAuthenticationChallenge) -> (URLSession.AuthChallengeDisposition, URLCredential?))?
var taskNeedNewBodyStream: ((URLSession, URLSessionTask) -> InputStream?)?
var taskDidCompleteWithError: ((URLSession, URLSessionTask, Error?) -> Void)?
taskWillPerformHTTPRedirection
首先来看一下第一个代理方法:
@objc(URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:)
func urlSession(
_ session: URLSession,
task: URLSessionTask,
willPerformHTTPRedirection response: HTTPURLResponse,
newRequest request: URLRequest,
completionHandler: @escaping (URLRequest?) -> Void)
{
var redirectRequest: URLRequest? = request
if let taskWillPerformHTTPRedirection = taskWillPerformHTTPRedirection {
redirectRequest = taskWillPerformHTTPRedirection(session, task, response, request)
}
completionHandler(redirectRequest)
}
根据函数名的意思,我们应该知道,这个函数就是用来处理重定向问题的,这个函数要求返回一个redirectRequest
,顾名思义,就是重定向Request
的,处理方式就是:如果给代理的重定向函数赋值了,就会返回代理函数的返回值,否则返回服务器的Request
。
taskDidReceiveChallenge
这个方法就是用来处理请求验证相关的,看一下:
@objc(URLSession:task:didReceiveChallenge:completionHandler:)
func urlSession(
_ session: URLSession,
task: URLSessionTask,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
{
var disposition: URLSession.AuthChallengeDisposition = .performDefaultHandling
var credential: URLCredential?
if let taskDidReceiveChallenge = taskDidReceiveChallenge {
(disposition, credential) = taskDidReceiveChallenge(session, task, challenge)
} else if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
let host = challenge.protectionSpace.host
if
let serverTrustPolicy = session.serverTrustPolicyManager?.serverTrustPolicy(forHost: host),
let serverTrust = challenge.protectionSpace.serverTrust
{
if serverTrustPolicy.evaluate(serverTrust, forHost: host) {
disposition = .useCredential
credential = URLCredential(trust: serverTrust)
} else {
disposition = .cancelAuthenticationChallenge
}
}
} else {
if challenge.previousFailureCount > 0 {
disposition = .rejectProtectionSpace
} else {
credential = self.credential ?? session.configuration.urlCredentialStorage?.defaultCredential(for: challenge.protectionSpace)
if credential != nil {
disposition = .useCredential
}
}
}
completionHandler(disposition, credential)
}
首先,我们来看一下
disposition
,它的类型是URLSession.AuthChallengeDisposition
,这个类型就是一个枚举类型:
@available(iOS 7.0, *)
public enum AuthChallengeDisposition : Int {
case useCredential /* Use the specified credential, which may be nil */
case performDefaultHandling /* Default handling for the challenge - as if this delegate were not implemented; the credential parameter is ignored. */
case cancelAuthenticationChallenge /* The entire request will be canceled; the credential parameter is ignored. */
case rejectProtectionSpace /* This challenge is rejected and the next authentication protection space should be tried; the credential parameter is ignored. */
}
useCredential
:使用的证书performDefaultHandling
:采用默认的方式,和服务器返回的authenticationMethod
有很大关系cancelAuthenticationChallenge
:取消认证rejectProtectionSpace
:拒绝认证
我们在进行验证的时候,有三种验证方式:
- 客户端验证
- 服务器验证
- 双向验证
从上面的函数方法,可以知道,如果服务器需要验证客户端,只需要给TaskDelegate
的taskDidReceiveChallenge
赋值就可以了。
在Alamofire
的双向验证中,客户端和服务端如果需要建立SSL
,只需要2步可以完成:
- 服务端返回
WWW-Authenticate
响应头,并返回自己信任证书- 客户端验证证书,然后用证书中的公钥把数据加密后发送给服务端
taskNeedNewBodyStream
同样的,我们先来看一下代码:
@objc(URLSession:task:needNewBodyStream:)
func urlSession(
_ session: URLSession,
task: URLSessionTask,
needNewBodyStream completionHandler: @escaping (InputStream?) -> Void)
{
var bodyStream: InputStream?
if let taskNeedNewBodyStream = taskNeedNewBodyStream {
bodyStream = taskNeedNewBodyStream(session, task)
}
completionHandler(bodyStream)
}
当给
task
的Request
提供一个body stream
时才会调用,我们不需要关心这个方法,即使我们通过fileURL
或者NSData
上传数据时,该函数也不会被调用。
taskDidCompleteWithError
一样的,先看代码:
@objc(URLSession:task:didCompleteWithError:)
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let taskDidCompleteWithError = taskDidCompleteWithError {
taskDidCompleteWithError(session, task, error)
} else {
if let error = error {
if self.error == nil { self.error = error }
if
let downloadDelegate = self as? DownloadTaskDelegate,
let resumeData = (error as NSError).userInfo[NSURLSessionDownloadTaskResumeData] as? Data
{
downloadDelegate.resumeData = resumeData
}
}
queue.isSuspended = false
}
}
该函数在请求完成后被调用,值得注意的是
error
不为nil
的情况,除了给自身的error
属性赋值外,针对下载任务做了特殊处理,就是把当前已经下载的数据保存在downloadDelegate.resumeData
中,有点像断点下载。
DataTaskDelegate
属性
首先,还是先来看一下它的属性:
// MARK: Properties
var dataTask: URLSessionDataTask { return task as! URLSessionDataTask }
override var data: Data? {
if dataStream != nil {
return nil
} else {
return mutableData
}
}
var progress: Progress
var progressHandler: (closure: Request.ProgressHandler, queue: DispatchQueue)?
var dataStream: ((_ data: Data) -> Void)?
private var totalBytesReceived: Int64 = 0
private var mutableData: Data
private var expectedContentLength: Int64?
dataTask: URLSessionDataTask
:DataTaskDelegate
管理URLSessionDataTask
data: Data?
:同样是返回Data
,但这里有一点不同,如果定义了dataStream
方法的话,这个data
返回为nil
progress: Progress
:进度,这个就不解释了progressHandler
:这不是函数,是一个元组,我们等下具体说说dataStream
:自定义的数据处理函数totalBytesReceived
:已经接收的数据mutableData
:保存数据的容器expectedContentLength
:接收的数据的总大小
生命周期
先看代码:
// MARK: Lifecycle
override init(task: URLSessionTask?) {
mutableData = Data()
progress = Progress(totalUnitCount: 0)
super.init(task: task)
}
override func reset() {
super.reset()
progress = Progress(totalUnitCount: 0)
totalBytesReceived = 0
mutableData = Data()
expectedContentLength = nil
}
这些很简单的,没有什么可说的。
方法调用
主要是看看它的函数方法:
// MARK: URLSessionDataDelegate
var dataTaskDidReceiveResponse: ((URLSession, URLSessionDataTask, URLResponse) -> URLSession.ResponseDisposition)?
var dataTaskDidBecomeDownloadTask: ((URLSession, URLSessionDataTask, URLSessionDownloadTask) -> Void)?
var dataTaskDidReceiveData: ((URLSession, URLSessionDataTask, Data) -> Void)?
var dataTaskWillCacheResponse: ((URLSession, URLSessionDataTask, CachedURLResponse) -> CachedURLResponse?)?
dataTaskDidReceiveResponse
func urlSession(
_ session: URLSession,
dataTask: URLSessionDataTask,
didReceive response: URLResponse,
completionHandler: @escaping (URLSession.ResponseDisposition) -> Void)
{
var disposition: URLSession.ResponseDisposition = .allow
expectedContentLength = response.expectedContentLength
if let dataTaskDidReceiveResponse = dataTaskDidReceiveResponse {
disposition = dataTaskDidReceiveResponse(session, dataTask, response)
}
completionHandler(disposition)
}
当收到服务端的响应后,该方法被触发。在这个函数中,我们能够获取到和数据相关的一些参数。
dataTaskDidBecomeDownloadTask
func urlSession(
_ session: URLSession,
dataTask: URLSessionDataTask,
didBecome downloadTask: URLSessionDownloadTask)
{
dataTaskDidBecomeDownloadTask?(session, dataTask, downloadTask)
}
在
dataTaskDidReceiveResponse
方法中,disposition
的类型是URLSession.ResponseDisposition
,这是一个枚举类型:
@available(iOS 7.0, *)
public enum ResponseDisposition : Int {
case cancel /* Cancel the load, this is the same as -[task cancel] */
case allow /* Allow the load to continue */
case becomeDownload /* Turn this request into a download */
@available(iOS 9.0, *)
case becomeStream /* Turn this task into a stream task */
}
因此,当我们设置成
becomeDownload
时,dataTaskDidBecomeDownloadTask
方法就会被调用,创建了一个新的downloadTask
。
dataTaskDidReceiveData
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
if initialResponseTime == nil { initialResponseTime = CFAbsoluteTimeGetCurrent() }
if let dataTaskDidReceiveData = dataTaskDidReceiveData {
dataTaskDidReceiveData(session, dataTask, data)
} else {
if let dataStream = dataStream {
dataStream(data)
} else {
mutableData.append(data)
}
let bytesReceived = Int64(data.count)
totalBytesReceived += bytesReceived
let totalBytesExpected = dataTask.response?.expectedContentLength ?? NSURLSessionTransferSizeUnknown
progress.totalUnitCount = totalBytesExpected
progress.completedUnitCount = totalBytesReceived
if let progressHandler = progressHandler {
progressHandler.queue.async { progressHandler.closure(self.progress) }
}
}
}
这个方法会把数据放入对象中,对自定义函数和进度信息进行处理。
dataTaskWillCacheResponse
func urlSession(
_ session: URLSession,
dataTask: URLSessionDataTask,
willCacheResponse proposedResponse: CachedURLResponse,
completionHandler: @escaping (CachedURLResponse?) -> Void)
{
var cachedResponse: CachedURLResponse? = proposedResponse
if let dataTaskWillCacheResponse = dataTaskWillCacheResponse {
cachedResponse = dataTaskWillCacheResponse(session, dataTask, proposedResponse)
}
completionHandler(cachedResponse)
}
该函数用于处理是否需要缓存响应,
Alamofire
默认是缓存这些response
的,但是每次发请求,它不会再缓存中读取。
DownloadTaskDelegate
属性
// MARK: Properties
var downloadTask: URLSessionDownloadTask { return task as! URLSessionDownloadTask }
var progress: Progress
var progressHandler: (closure: Request.ProgressHandler, queue: DispatchQueue)?
var resumeData: Data?
override var data: Data? { return resumeData }
var destination: DownloadRequest.DownloadFileDestination?
var temporaryURL: URL?
var destinationURL: URL?
var fileURL: URL? { return destination != nil ? destinationURL : temporaryURL }
有和上面重复的属性,我就不说了,看下没有重复的部分:
downloadTask
:和URLSessionDownloadDelegate
相对应的URLSessionDownloadTask
resumeData
:在上边我们提到过,当请求完成后,如果error
不为nil
,如果是DownloadTaskDelegate
,就会给这个属性赋值data
:返回resumeData
destination
:通过这个函数可以自定义文件保存目录和保存方式,这个保存方式分两种,为URl
创建文件夹,删除已经下载且存在的文件,这个会在后续的文章中提到
temporaryURL
:临时的URL
destinationURL
:数据存储URL
fileURL
:fileURL
返回文件的路径,如果destination
不为nil
,就返回destinationURL
,否则返回temporaryURL
生命周期
// MARK: Lifecycle
override init(task: URLSessionTask?) {
progress = Progress(totalUnitCount: 0)
super.init(task: task)
}
override func reset() {
super.reset()
progress = Progress(totalUnitCount: 0)
resumeData = nil
}
方法调用
代理方法有三个:
// MARK: URLSessionDownloadDelegate
var downloadTaskDidFinishDownloadingToURL: ((URLSession, URLSessionDownloadTask, URL) -> URL)?
var downloadTaskDidWriteData: ((URLSession, URLSessionDownloadTask, Int64, Int64, Int64) -> Void)?
var downloadTaskDidResumeAtOffset: ((URLSession, URLSessionDownloadTask, Int64, Int64) -> Void)?
downloadTaskDidFinishDownloadingToURL
func urlSession(
_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didFinishDownloadingTo location: URL)
{
temporaryURL = location
guard
let destination = destination,
let response = downloadTask.response as? HTTPURLResponse
else { return }
let result = destination(location, response)
let destinationURL = result.destinationURL
let options = result.options
self.destinationURL = destinationURL
do {
if options.contains(.removePreviousFile), FileManager.default.fileExists(atPath: destinationURL.path) {
try FileManager.default.removeItem(at: destinationURL)
}
if options.contains(.createIntermediateDirectories) {
let directory = destinationURL.deletingLastPathComponent()
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
}
try FileManager.default.moveItem(at: location, to: destinationURL)
} catch {
self.error = error
}
}
当数据下载完成后,该函数被触发。系统会把数据下载到一个临时的
locationURL
的地方,我们就是通过这个URL
拿到数据的。上边函数内的代码主要是把数据复制到目标路径中。
downloadTaskDidWriteData
func urlSession(
_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didWriteData bytesWritten: Int64,
totalBytesWritten: Int64,
totalBytesExpectedToWrite: Int64)
{
if initialResponseTime == nil { initialResponseTime = CFAbsoluteTimeGetCurrent() }
if let downloadTaskDidWriteData = downloadTaskDidWriteData {
downloadTaskDidWriteData(
session,
downloadTask,
bytesWritten,
totalBytesWritten,
totalBytesExpectedToWrite
)
} else {
progress.totalUnitCount = totalBytesExpectedToWrite
progress.completedUnitCount = totalBytesWritten
if let progressHandler = progressHandler {
progressHandler.queue.async { progressHandler.closure(self.progress) }
}
}
}
该代理方法在数据下载过程中被触发,主要的作用就是提供下载进度。
downloadTaskDidResumeAtOffset
func urlSession(
_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didResumeAtOffset fileOffset: Int64,
expectedTotalBytes: Int64)
{
if let downloadTaskDidResumeAtOffset = downloadTaskDidResumeAtOffset {
downloadTaskDidResumeAtOffset(session, downloadTask, fileOffset, expectedTotalBytes)
} else {
progress.totalUnitCount = expectedTotalBytes
progress.completedUnitCount = fileOffset
}
}
如果一个下载的
task
是可以恢复的,那么当下载被取消或者失败后,系统会返回一个resumeData
对象,这个对象包含了一些跟这个下载task
相关的一些信息,有了它就能重新创建下载task
,创建方法有两个:downloadTask(withResumeData:)
和downloadTask(withResumeData:completionHandler:)
,当task
开始后,上边的代理方法就会被触发。
UploadTaskDelegate
属性
// MARK: Properties
var uploadTask: URLSessionUploadTask { return task as! URLSessionUploadTask }
var uploadProgress: Progress
var uploadProgressHandler: (closure: Request.ProgressHandler, queue: DispatchQueue)?
这些属性又得已经重复了,就不多说了。
生命周期
// MARK: Lifecycle
override init(task: URLSessionTask?) {
uploadProgress = Progress(totalUnitCount: 0)
super.init(task: task)
}
override func reset() {
super.reset()
uploadProgress = Progress(totalUnitCount: 0)
}
这也不用多说啥,主要的还是看看方法调用。
方法调用
这里只有一个方法调用:
// MARK: URLSessionTaskDelegate
var taskDidSendBodyData: ((URLSession, URLSessionTask, Int64, Int64, Int64) -> Void)?
func URLSession(
_ session: URLSession,
task: URLSessionTask,
didSendBodyData bytesSent: Int64,
totalBytesSent: Int64,
totalBytesExpectedToSend: Int64)
{
if initialResponseTime == nil { initialResponseTime = CFAbsoluteTimeGetCurrent() }
if let taskDidSendBodyData = taskDidSendBodyData {
taskDidSendBodyData(session, task, bytesSent, totalBytesSent, totalBytesExpectedToSend)
} else {
uploadProgress.totalUnitCount = totalBytesExpectedToSend
uploadProgress.completedUnitCount = totalBytesSent
if let uploadProgressHandler = uploadProgressHandler {
uploadProgressHandler.queue.async { uploadProgressHandler.closure(self.uploadProgress) }
}
}
}
该函数主要目的是提供上传的进度,在
Alamofire
中,上传数据用的是stream
,这个会在后续文章中给出详细的解释。
总结
学习框架的时候,好多东西当时记住了,可能一会儿就忘了,写下这篇文章的目的,就是为了加深理解印象,方便以后查阅笔记,如果文章有错误,还望指出,还得感谢一下这位朋友马在路上。