原文地址:https://developer.apple.com
说明:网上也有对这篇文章的翻译,但是我觉得翻译的不太准确,所以自己翻译下,还可以加深对 URL Session 的理解,也会写一些自己的理解在里面,如果您发现翻译不对或者知识理解的有误,请您一定要告诉我。
你有两种方式使用 NSURLSession 的 API:用系统的代理或者自己实现的代理。通常,如果你的 app 要做下面这些事的话,就一定要使用你自己的代理了:
- 在你的app没有运行时,使用后台 sessions 去下载或者上传。
- 执行自定义的认证 authentication
- 执行自定义的 SSL 证书验证
- 决定是否将被下载的内容转移到磁盘或被显示在基于 MIME 类型返回的服务还是其它相似的标准上。
- 用一个 body stream 上传数据(相反的是一个 NSData 的对象)
- 制定缓存策略
- 制定 HTTP 重定向策略
(旁白:说明下上面的几种情况)
第二点的authentication我理解的是 HTTP Basic 和 HTTP Digest 参考
看段 Alamofire 的代码
public func authenticate(
user user: String,
password: String,
persistence: NSURLCredentialPersistence = .ForSession)
-> Self
{
let credential = NSURLCredential(user: user, password: password, persistence: persistence)
return authenticate(usingCredential: credential)
}
func URLSession(
session: NSURLSession,
task: NSURLSessionTask,
didReceiveChallenge challenge: NSURLAuthenticationChallenge,
completionHandler: ((NSURLSessionAuthChallengeDisposition, NSURLCredential?) -> Void))
{
var disposition: NSURLSessionAuthChallengeDisposition = .PerformDefaultHandling
var credential: NSURLCredential?
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?.serverTrustPolicyForHost(host),
serverTrust = challenge.protectionSpace.serverTrust
{
//...省略
} else {
if challenge.previousFailureCount > 0 {
disposition = .CancelAuthenticationChallenge
} else {
//在这里
credential = self.credential ?? session.configuration.URLCredentialStorage?.defaultCredentialForProtectionSpace(challenge.protectionSpace)
if credential != nil {
disposition = .UseCredential
}
}
}
completionHandler(disposition, credential)
}
//调用
var URLString = "https://httpbin.org/basic-auth/\(user)/\(password)"
var URLString = "https://httpbin.org/digest-auth/\(qop)/\(user)/\(password)"
Alamofire.request(.GET, URLString)
.authenticate(user: user, password: password)
.response { responseRequest, responseResponse, responseData, responseError in
request = responseRequest
response = responseResponse
data = responseData
error = responseError
expectation.fulfill()
}
第三点的SSL 证书验证我理解的是服务端证书的校验 参考文章
看段代码
public func URLSession(
session: NSURLSession,
didReceiveChallenge challenge: NSURLAuthenticationChallenge,
completionHandler: ((NSURLSessionAuthChallengeDisposition, NSURLCredential?) -> Void))
{
var disposition: NSURLSessionAuthChallengeDisposition = .PerformDefaultHandling
var credential: NSURLCredential?
if let sessionDidReceiveChallenge = sessionDidReceiveChallenge {
(disposition, credential) = sessionDidReceiveChallenge(session, challenge)
} else if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
let host = challenge.protectionSpace.host
if let
serverTrustPolicy = session.serverTrustPolicyManager?.serverTrustPolicyForHost(host),
serverTrust = challenge.protectionSpace.serverTrust
{
if serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) {
disposition = .UseCredential
credential = NSURLCredential(forTrust: serverTrust)
} else {
disposition = .CancelAuthenticationChallenge
}
}
}
completionHandler(disposition, credential)
}
还有第二点里提到的 task 回调的验证挑战,使用了证书验证就不会使用 HTTP 的认证了。在看下 ServerTrustPolicy.swift 类。详细写了怎样验证服务证书。
如果你的 app 不需要做这些事情,使用系统代理就可以了(也就是不用自己实现代理方法)。根据你先择的技术,看下对应的内容:
- Life Cycle of a URL Session with System-Provided Delegates 提供了轻量级的方法创建并使用 URL session 。即使你打算自己写代理,也应该看下这个章节,因为它给了你配置和使用这个对象完整的说明。
使用系统代理的一个 URL Session 的生命周期
如果你在使用 NSURLSession 类的时候没有设置代理,那系统代理会为你处理大部分细节。在使用系统代理 NSURLSesson 的时候会接受一系列的完成处理的回调方法:
1,创建一个 session configuration 。如果是后台 session ,那这个configuration 必须包含一个唯一标识。存储这个标识,如果程序崩溃,终止或者挂起还可以在找到这个后台 session。
2,创建一个 session,指定一个 configuration 对象 且 delegate 为 nil。
3,在 session 里创建任务 (task) 对象,相当于每一个资源请求。
每个 task 任务开始都是挂起状态的。之后你 app 的task任务调用 resume 才开始下载指定的资源。
这个 task 任务是个子类,有 NSURLSessionTask - NSURLSessionDataTask,NSURLSessionUploadTask,NSURLSessionDownloadTask,你可以按照这些层级去创建。这些对象相当于 NSURLConnection 对象,但是比 NSURLConnection 给你更多的控制和一个统一标准的代理模型。
尽管你的app可以添加更多的任务在 session 里,为了简单起见,还是用一个单独的 task 任务来描述剩下的生命周期。
重要:如果你在使用 NSURLSession 时没有使用代理,你的 app 在创建带回调的 task 任务时,要带上一个 ** completionHandler** 参数,否则无法获取到数据。
4,一个下载任务,在从服务器下载期间,如果用户暂停了任务,使用 cancelByProducingResumeData: 方法取消了任务。之后可以通过 downloadTaskWithResumeData: 或者 downloadTaskWithResumeData:completionHandler: 返回 resume data 来创建一个新的下载任务继续下载。
5,在一个任务完成时, NSURLSession 对象会调用 task 任务的 completion handler 回调。
注意:NSURLSession不会报告服务错误,你的 app 接受到的唯一错误是客户端错误,例如没法解析主机名或连不到主机。错误代码的描述 参考这里。
服务端错误通过 NSHTTPURLResponse 对象的 HTTP 状态码来报告。要了解更多,请阅读 NSHTTPURLResponse 和 NSURLResponse 类。
6,当你的 app 不再需要一个 session 时,调用 invalidateAndCancel 方法来使其失效,并取消未完成的任务。或者调用 finishTasksAndInvalidate 方法使其失效,并在失效前允许完成那些未完成的任务。
一个使用自定义代理的 URL Session 的生命周期
你经常使用没有代理的 NSURLSession 。然而,如果你要使用 NSURLSession API 进行后台的下载和上传,或者你要去处理 * authentication* 鉴定 或者 非默认方式的缓存,你就必须要设置一个代理去遵守这个 session delegate 的协议,一个或多个 task delegate 的协议 或者 这些协议的组合。这些代理提供了很多作用:
在使用下载功能的时候,这个 NSURLSession 对象利用代理提供给 app 下载好的数据的 URL文件路径。
代理能提供鉴定挑战 authentication challenges。
代理为基于 stream 上传远程服务的任务, 提供 body streams。
代理能决定要不要 HTTP 重定向。
NSURLSession 可以利用代理告诉 app 每个交易请求的状态。
一个 Data task 任务接受一个初始的回调,在哪里你可以将这个 request 请求转成一个下载任务且随之调用,从远程服务获取这个数据。代理是 NSURLSession 对象告诉 app 一个请求完成的一种方式。
如果你使用一个自定义代理的 URL session(为了后台任务),这个 URL session 完整的生命周期会很复杂。在使用自定义代理的 NSURLSession 时,你的app要接受一系列的代理方法的调用:
1,创建一个 session configuration。对于后台 session,这个 configuration 必须包含一个唯一标识。存储这个标识,如果你的 app 崩溃或者终止或者挂起,可以使用它恢复session。
2,创建一个 session,指定一个 configuration 对象且设置一个代理。
3,在 session 里创建任务 (task) 对象,相当于每一个资源请求。
每个 task 任务开始都是挂起状态的。之后你 app 的task任务调用 resume 才开始下载指定的资源。
这个 task 任务是个子类,有 NSURLSessionTask - NSURLSessionDataTask,NSURLSessionUploadTask,NSURLSessionDownloadTask,你可以按照这些层级去创建。这些对象相当于 NSURLConnection 对象,但是比 NSURLConnection 给你更多的控制和一个统一标准的代理模型。
尽管你的 app 可以添加更多的任务在 session 里,为了简单起见,还是用一个单独的 task 任务来描述剩下的生命周期。
4,如果远程服务返回了一个必须要鉴定 authentication 的状态码,且这个鉴定 authentication 必须是一个链接级的挑战 challenge(例如一个SSL 客户端证书),NSURLSession 会调用一个 authentication challenge 的代理方法。
session级的挑战 challenge 有-NSURLAuthenticationMethodNTLM,NSURLAuthenticationMethodNegotiate,NSURLAuthenticationMethodClientCertificate,或者NSURLAuthenticationMethodServerTrust -这个 NSURLSession 对象会调用 session 的代理方法 URLSession:didReceiveChallenge:completionHandler:。如果你的 app 没有提供这个 session 的代理方法,这个 NSURLSession 对象会调用 task 的代理方法 URLSession:task:didReceiveChallenge:completionHandler: 来处理这个挑战 challenge。
不是 session级的挑战(所有其它的情况),这个 NSURLSession 会调用 URLSession:task:didReceiveChallenge:completionHandler: 方法去处理挑战。如果你的 app 提供一个 session 代理 且 需要去处理鉴定 authentication 。然后你必须要在任务级别上处理或者在调用每个会话的处理时提供任务级的处理(旁白:每个任务都会调用)。session 的代理方法 URLSession:didReceiveChallenge:completionHandler: 在非 session 级上的挑战不会被调用。
如果一个上传任务鉴定 authentication 失败,如果要从 stream 上提供任务的数据,这个 NSURLSession 对象会调用 URLSession:task:needNewBodyStream: 方法。这个代理必须为 new request 的 body data 提供一个新的 NSInputStream 对象。
关于写一个 NSURLSession 鉴定 authentication 的代理方法更多的信息读一下 这个。
5,接到一个 HTTP 重定向的响应,这个 NSURLSession 对象的代理会调用 URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler: 方法。这个代理方法从提供来的 NSURLRequest 对象(去接受这个重定向),一个新的 NSURLRequest 对象(去重定向一个不同的 URL),或者 nil(将重定向的响应 body 作为一个有效的响应,且返回它作为一个结果)里面的一个调用 completion handler 返回。
- 如果这个重定向被接受,会返回到第四步(处理鉴定挑战)
- 如果代理没有实现这个方法。这个重定向将接受重定向数的最大数。
6,一个重新下载的任务调用 downloadTaskWithResumeData: 或者 downloadTaskWithResumeData:completionHandler: 来创建。NSURLSession 会调用代理的 URLSession:downloadTask:didResumeAtOffset:expectedTotalBytes:方法将这个新任务传过来。
7,一个 data task 任务,这个 NSURLSession 对象调用代理的 URLSession:dataTask:didReceiveResponse:completionHandler: 方法。决定是否将一个data task 任务转成一个下载任务,且调用 completion 的回调是继续一个 data 任务还是 下载任务。
8,如果是使用 uploadTaskWithStreamedRequest: 创建的任务。NSURLSession 调用代理的 URLSession:task:needNewBodyStream: 方法来提供 body data。
9,在上传内容到服务期间,代理会定时调用 URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend: 方法来报告上传进度。
10,在与服务交互期间,任务代理定期的接受一个回调来报告交互的进度。下载任务会调用代理的 URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite: 方法传来一个成功写入到磁盘的字节数。一个 data 任务,则会调用代理的 URLSession:dataTask:didReceiveData: 方法返回一些预期的数据。
一个下载任务在交互期间,如果用户告诉你的 app 暂停下载,调用 cancelByProducingResumeData: 方法来取消任务。
之后,如果用户要请求 app 恢复下载,通过向 downloadTaskWithResumeData: 或者 downloadTaskWithResumeData:completionHandler: 方法传入 resume data,去创建一个新的下载任务继续下载,然后返回到第三步(创建并恢复任务)。
11,一个 data 任务,NSURLSession 对象调用代理的 URLSession:dataTask:willCacheResponse:completionHandler: 方法,决定是否允许缓存。如果不实现这个方法,默认行为是去使用在 session 的 configuration 对象里指定的缓存策略。
12,如果一个下载任务成功完成,然后这个 NSURLSession 对象调用任务的 URLSession:downloadTask:didFinishDownloadingToURL: 方法同时传来一个临时文件的地址。你的 app 必须读取这个数据或者在代理方法 return 之前将它移动到 app 的沙箱目录里。
13,在一些任务完成的时候,这个 NSURLSession 对象调用代理的 URLSession:task:didCompleteWithError: 方法传来一个 error 对象或者 nil (如果这个任务成功完成)。
如果任务失败,大部分 app 会重试这个请求直到用户取消下载或在请求一定不会成功的情况下返回一个 error。然而,你的 app 不应该马上重试,而是,使用 reachability APIs 去判断服务是否可用,且在接到 reachability 改变通知的时候新建一个请求 。
如果下载任务能被恢复, NSError 对象的 userInfo 里会包含一个 key 是 NSURLSessionDownloadTaskResumeData 的值。你的 app 应该通过这个值去调用 downloadTaskWithResumeData: 或 downloadTaskWithResumeData:completionHandler: 去创建新任务继续下载。
如果任务不能被恢复,你的 app 应该新建一个下载任务重新开始。
在任何情况下,如果是除了服务错误以外的任何原因交互失败,返回第三步(创建并恢复任务对象)。
14,如果响应是多部分编码 multipart encoded 的,这个 session 将再次调用代理的 * didReceiveResponse* 方法,接下来有0或者多次额外的 didReceiveData 的调用。如果这个情况发生,返回第七步(处理 didReceiveResponse 方法)。
15,当你不在需要一个 session 的时候,调用 invalidateAndCancel 终止它(取消未完成的任务)或者调用 finishTasksAndInvalidate (允许未完成的任务在 session 对象终止之前完成任务)。
终止 session 之后,所有未完成的任务被取消或者已经完成, session 会发送代理的 URLSession:didBecomeInvalidWithError: 消息。在代理方法返回的时候,session 处理对代理的强引用。
重要:这个 session 对象保持了对代理的强引用直到 session 被终止。如果你不终止这个 session,将会内存泄漏。
如果你的 app 取消一个正在下载的任务,这个 NSURLSession 对象会调用 URLSession:task:didCompleteWithError: 方法返回一个 error。
(旁白:像第十三步)