NSURLSession及其相关的类提供了通过HTTP下载内容的API。这个类提供了丰富的委托方法来支持验证、以及在后台执行下载的能力(当app没有运行,或者iOS app被挂起的时候)。
使用NSURLSession API,app创建一些列的会话,它们每一个都协调一组关联的数据传输任务。例如,如果你正在编写web浏览器,app可以为每个标签或窗口创建一个会话。在每个会话中,app添加一系列的任务,每个任务负责一个指定URL请求(如果原始URL返回HTTP重定向,那就是任何后续的URL)。
和大多数网络API一样,NSURLSession API是高度依赖异步操作的。如果你使用默认配置,系统提供的委托,你必须提供完成处理器代码块,以便在传输完成的时候或出现错误的时候,将数据返回给app。或者,如果你提供自定义的委托对象,则任务对象调用这些委托方法时使用从服务器接收到的数据(或传输完成时,从文件下载)。
注意:完成回调主要用于使用自定义委托的替代方法。如果你使用完成回调的方法创建一个任务,那么用于响应和数据传递的委托方法将不会被调用。
NSURLSession API提供状态和进度属性,并传递这个信息给委托。它支持取消、重启(恢复)、一起挂起任务,并且它提供在挂起、取消、或者下载错误处重新恢复任务的功能。
理解URL会话概念
会话中任务的行为基于三个因素:会话(由创建它的配置对象的类型决定)的类型、任务的类型、以及当任务创建的时候app是否在前台。
会话类型
NSURLSession API支持三种会话类型,由用于创建会话的配置对象的类型决定:
- 默认会话行为与其他下载URL的Foundation方法很类似。它们使用永久的基于磁盘的缓存,并在用户的钥匙串中存储证书。
- 短暂会话不会存储任何数据到磁盘:所有缓存、证书存储等等都保存在RAM中,并与会话捆绑。因此,当app使会话无效时,它们将会被自动清除。
- 除了要使用独立进程处理所有数据传输以外,后台会话和默认会话很类似。后台会话有一些额外的限制,详情见Background Transfer Considerations。
任务类型
在会话中,NSURLSession类支持三种任务类型:数据任务(data tasks)、下载任务(download tasks)、以及上传任务(upload tasks)。
- 数据任务使用NSData对象来发送和接收数据。数据任务被用于从app到服务器的短的、需要经常交互的请求。数据任务可以在每接到一部分数据的时候就把它返回给app,也可以通过完成处理器一次性返回所有的数据。
- 下载任务以文件的形式获取数据,并且在app没有运行的时候支持后台下载。
- 上传任务以文件的形式发送数据,并且在app没有运行的时候支持后台上传。
后台传输注意事项
NSURLSession类支持在app被挂起的时候在后台传输。后台传输只有在会话任务是使用后台会话配置对象(通过调用backgroundSessionConfiguration:返回的对象)创建的时候才会被支持。
由于实际传输是通过独立的进程执行的,并且重器app的进程非常昂贵,所以使用后台会话,一些功能将不可用,这导致了以下限制:
- 会话必须为事件传递提供委托。(对于上传和下载,委托行为和进程内传输是一样的。)
- 只支持HTTP和HTTPS协议(没有自定义协议)。
- 始终遵循重定向。
- 只支持文件的上传任务(在程序退出之后,上传数据对象、或者数据流都将失败)。
- 如果后台传输是在app在后台的时候被启动的,那么配置对象的discretionary属性将被是作为true。
注意: 在iOS 8和OS X10.10之前,不支持在后台执行数据任务。
当app重启时,它的行为的方式在iOS和OS X之间有略微的不同。
在iOS中,当后台传输完成或需要证书时,如果app没有在运行,iOS会自动在后台重启app,并调用app的UIApplicationDelegate对象中的application:handleEventsForBackgroundURLSession:completionHandler:方法。这个调用提供导致app重启的会话的识别码。App应该存储完成处理器,使用相同的标识符创建后台配置对象,并使用这个配置对象创建会话。这个新的会话会自动与正在运行的后台活动相关联。之后,当会话完成最后一个后台下载任务的时候,它会给会话委托发送一个URLSessionDidFinishEventsForBackgroundURLSession:消息。在委托方法中,调用之前在主线程中存储的完成处理器,以便让操作系统知道可以再次安全的挂起app。
无论在iOS还时OS X中,当应用重启app的时候,app应该立即使用相同的标识符(这个标识符是app上次运行时未完成任务的任何会话的)创建后台配置对象,然后为每个配置对象创建一个会话。这些新的会话也会自动与正在进行的后台活动重新关联。
注意:你必须为每个标识符(在创建配置对象的时候指定)创建会话。共享同一个标识符的多个会话的行为尚未被定义。
如果app在挂起期间完成任务,委托的URLSession:downloadTask:didFinishDownloadingToURL:方法将被调用,这个方法会使用与最新下载的文件相关联的任务(task)和URL。
类似的,如果任务请求验证,NSURLSession对象调用委托相应的URLSession:task:didReceiveChallenge:completionHandler: 或 URLSession:didReceiveChallenge:completionHandler:方法。
如果网络发生错误,在后台会话中的上传和下载任务会通过URL加载系统自动重试。没有必要使用可达性API来确定合适重试失败的任务。
对于如何使用NSURLSession在后台进行传输数据的例子,参见Simple Background Transfer。
生命周期和委托交互
根据你正在使用NSURLSession类所做的事,可能对你充分理解会话生命周期会有帮助,包括会话如何与它的委托交互、委托调用的顺序、当收到服务器返回重定向的时候发生了什么、当app恢复一个失败下载的时候发生了什么等等。
关于URL会话生命周期的完整描述,请阅读Life Cycle of a URL Session。
NSCopying行为
会话和任务对象遵守NSCoping协议,如下所述:
- 当app拷贝会话或任务对象时,你会得到相同的对象。
- 当app拷贝配置对象时,你会得到一个新的副本,你可以对它进行独立的修改。
委托类接口的示例
下面任务部分中的代码片段,都基于代码清单1-1中的类接口。
代码清单1-1 委托类接口的示例
@import Foundation;
NS_ASSUME_NONNULL_BEGIN
typedef void (^CompletionHandler)();
@interface MySessionDelegate : NSObject <NSURLSessionDelegate, NSURLSessionTaskDelegate, NSURLSessionDataDelegate, NSURLSessionDownloadDelegate, NSURLSessionStreamDelegate>
@property NSMutableDictionary <NSString *, CompletionHandler>*completionHandlers;
@end
NS_ASSUME_NONNULL_END
import Foundation
typealias CompletionHandler = () -> Void
class MySessionDelegate : NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionDataDelegate, URLSessionDownloadDelegate, URLSessionStreamDelegate {
var completionHandlers: [String: CompletionHandler] = [:]
}
创建和配置会话
NSURLSession API提供了广泛的配置选项:
- 以特定于单一会话的方式为缓存、cookie、证书、以及协议提供存储支持
- 与特定请求(任务)或一组请求(会话)绑定的验证
- 通过URL上传和下载文件,推荐将数据(文件的内容)从元数据(URL和设置)中分离出来
- 配置每个主机的最大连接数
- 如果整个资源在相当长的时间里没有被下载,则会触发该资源的超时
- 支持TLS的最小和最大版本
- 自定义代理字典
- 控制cookie策略
- 控制HTTP渠道(pipelining)行为
因为大多数设置被包含在独立的配置对象中,所以你通常可以重用这些设置。当你实例化一个会话对象的时候,请指定如下内容:
- 一个用于管理会话行为及其任务的配置对象
- 可选的,委托对象用来处理接收到的传入数据,并处理特定于会话及其任务的其他事件(例如服务器验证、确定是否将资源下载请求转换到下载)。如果你不能够提供委托,NSURLSession对象会使用系统提供的委托。这样,你可以轻松得使用NSURLSession替换使用NSURLSession上的sendAsynchronousRequest:queue:completionHandler:方便方法的现有代码。
注意:如果app需要执行后台传输,它必须提供自定义委托。
当你实例化会话对象之后,除非创建一个新的会话,否则你不能改变该配置或者委托。
代码清单1-2 显示了如何创建一般的、短暂的、和后台会话的例子。
代码清单1-2 创建和配置会话
// Creating session configurations
NSURLSessionConfiguration *defaultConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSessionConfiguration *ephemeralConfiguration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
NSURLSessionConfiguration *backgroundConfiguration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier: @"com.myapp.networking.background"];
// Configuring caching behavior for the default session
NSString *cachesDirectory = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject;
NSString *cachePath = [cachesDirectory stringByAppendingPathComponent:@"MyCache"];
/* Note:
iOS requires the cache path to be
a path relative to the ~/Library/Caches directory,
but OS X expects an absolute path.
*/
#if TARGET_OS_OSX
cachePath = [cachePath stringByStandardizingPath];
#endif
NSURLCache *cache = [[NSURLCache alloc] initWithMemoryCapacity:16384 diskCapacity:268435456 diskPath:cachePath];
defaultConfiguration.URLCache = cache;
defaultConfiguration.requestCachePolicy = NSURLRequestUseProtocolCachePolicy;
// Creating sessions
id <NSURLSessionDelegate> delegate = [[MySessionDelegate alloc] init];
NSOperationQueue *operationQueue = [NSOperationQueue mainQueue];
NSURLSession *defaultSession = [NSURLSession sessionWithConfiguration:defaultConfiguration delegate:delegate operationQueue:operationQueue];
NSURLSession *ephemeralSession = [NSURLSession sessionWithConfiguration:ephemeralConfiguration delegate:delegate delegateQueue:operationQueue];
NSURLSession *backgroundSession = [NSURLSession sessionWithConfiguration:backgroundConfiguration delegate:delegate delegateQueue:operationQueue];
// Creating session configurations
let defaultConfiguration = URLSessionConfiguration.default()
let ephemeralConfiguration = URLSessionConfiguration.ephemeral()
let backgroundConfiguration = URLSessionConfiguration.backgroundSessionConfiguration(withIdentifier: "com.myapp.networking.background")
// Configuring caching behavior for the default session
let cachesDirectoryURL = FileManager.default().urlsForDirectory(.cachesDirectory, inDomains: .userDomainMask).first!
let cacheURL = try! cachesDirectoryURL.appendingPathComponent("MyCache")
var diskPath = cacheURL.path
/* Note:
iOS requires the cache path to be
a path relative to the ~/Library/Caches directory,
but OS X expects an absolute path.
*/
#if os(OSX)
diskPath = cacheURL.absoluteString
#endif
let cache = URLCache(memoryCapacity:16384, diskCapacity: 268435456, diskPath: diskPath)
defaultConfiguration.urlCache = cache
defaultConfiguration.requestCachePolicy = .useProtocolCachePolicy
// Creating sessions
let delegate = MySessionDelegate()
let operationQueue = OperationQueue.main()
let defaultSession = URLSession(configuration: defaultConfiguration, delegate: delegate, delegateQueue: operationQueue)
let ephemeralSession = URLSession(configuration: ephemeralConfiguration, delegate: delegate, delegateQueue: operationQueue)
let backgroundSession = URLSession(configuration: backgroundConfiguration, delegate: delegate, delegateQueue: operationQueue)
除了后台配置外,你可以复用会话配置对象来创建其他会话。(你不能重用后台会话配置,因为标识符不同。)
你还可以在任何时候安全的修改配置对象。当你创建会话的时候,会话对配置对象执行了深拷贝,所以修改只影响新会话。例如,如果你希望只在Wi-Fi连接时检索某个内容,你就可以为该内容创建第二个会话。如代码清单1-3所示。
代码清单1-3 使用相同的配置对象创建第二个会话
ephemeralConfiguration.allowsCellularAccess = NO;
NSURLSession *ephemeralSessionWiFiOnly = [NSURLSession sessionWithConfiguration:ephemeralConfiguration delegate:delegate delegateQueue:operationQueue];
ephemeralConfiguration.allowsCellularAccess = false
let ephemeralSessionWiFiOnly = URLSession(configuration: ephemeralConfiguration, delegate: delegate, operationQueue: operationQueue)
使用系统提供的委托获取资源
使用NSURLSession最直接的方式就是实用系统提供的委托请求资源。实用这个方法,你只需要在app中提供两段代码:
- 创建配置对象以及使用该对象创建会话的代码
- 完成处理器,在数据接收完成后执行某些操作
实用系统提供的委托,每个请求只需要一行代码你就可以获取特定的URL。代码清单1-4显示了这种简化模式的示例。
注意:系统提供的委托仅提供有限的网络行为的定制。如果app有超过基本URL获取的需求,例如自定义验证、或后台下载,这个技术就不太适用。有关必须实现的各种委托情况的列表,参见Life Cycle of a URL Session。
清单 1-4 适用系统提供的委托请求资源
NSURLSession *sessionWithoutADelegate = [NSURLSession sessionWithConfiguration:defaultConfiguration];
NSURL *url = [NSURL URLWithString:@"https://www.example.com/"];
[[sessionWithoutADelegate dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSLog(@"Got response %@ with error %@.\n", response, error);
NSLog(@"DATA:\n%@\nEND DATA\n", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
}] resume];
let sessionWithoutADelegate = URLSession(configuration: defaultConfiguration)
if let url = URL(string: "https://www.example.com/") {
(sessionWithoutADelegate.dataTask(with: url) { (data, response, error) in
if let error = error {
print("Error: \(error)")
} else if let response = response,
let data = data,
let string = String(data: data, encoding: .utf8) {
print("Response: \(response)")
print("DATA:\n\(string)\nEND DATA\n")
}
}).resume()
}
使用自定义委托获取数据
如果你使用自定义委托来检索数据,该委托必须实现下面方法中的至少一种:
- URLSession:dataTask:didReceiveData:将请求中的数据提供给任务,一次一段。
- URLSession:task:didCompleteWithError:向你的任务表明数据已经接收完成。
如果app需要使用URLSession:dataTask:didReceiveData:方法返回的数据,你的代码负责通过某种方式存储该数据。
例如,web浏览器或许需要显示到达的数据,以及之前接收到的数据。为此,它或许使用字典,把数据映射到NSMutableData对象来储存结果,然后使用这些对象的appendData:方法来添加新接收到的数据。
代码清单1-5 显示了你如何创建和开始一个数据任务。
代码清单1-5 数据任务示例
NSURL *url = [NSURL URLWithString: @"https://www.example.com/"];
NSURLSessionDataTask *dataTask = [defaultSession dataTaskWithURL:url];
[dataTask resume];
if let url = URL(string: "https://www.example.com/") {
let dataTask = defaultSession.dataTask(with: url)
dataTask.resume()
}
下载文件
在高级别中,下载文件和接收数据很类似。App应该实现下列委托方法:
- URLSession:downloadTask:didFinishDownloadingToURL:给下载内容的临时文件提供用于存储的app的URL。
重要:在该方法返回之前,必须打开文件以进行读取,或把它移动到永久位置。当该方法返回时,删除临时文件。
- URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:提供给应用关于下载进程的状态信息。
- URLSession:downloadTask:didResumeAtOffset:expectedTotalBytes:告诉app它试图恢复之前失败的下载已成功。
- URLSession:task:didCompleteWithError:告诉app下载失败。
如果你在后台会话中安排下载,那么当app不运行时该下载仍会继续进行。如果你把下载安排在标准或短暂会话中,该下载必须是在app重启的时候才会重新开始。
在服务器传输期间,如果用户暂停了下载,app可以通过调用cancelByProducingResumeData:方法来取消任务。之后,app可以把返回的数据传递给downloadTaskWithResumeData:或者downloadTaskWithResumeData:completionHandler:方法,来创建新的下载任务来继续下载。
如果传输失败,你委托的URLSession:task:didCompleteWithError:方法被调用。如果任务是可恢复的,那么对象的userInfo字典包含一个NSURLSessionDownloadTaskResumeData键的值;应用可以把返回的数据传递给downloadTaskWithResumeData: 或 downloadTaskWithResumeData:completionHandler:方法来创建一个新的下载任务,来重试该下载。
代码清单1-6提供一个下载一般大文件的示例。代码清单1-7提供下载任务委托方法的示例。
代码清单1-6 下载任务示例
NSURL *url = [NSURL URLWithString:@"https://developer.apple.com/library/ios/documentation/Cocoa/Reference/Foundation/ObjC_classic/FoundationObjC.pdf"];
NSURLSessionDownloadTask *downloadTask = [backgroundSession downloadTaskWithURL:url];
[downloadTask resume];
if let url = URL(string: "https://www.example.com/") {
let dataTask = defaultSession.dataTask(with: url)
dataTask.resume()
}
代码清单 1-7 下载任务的委托方法
- (void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didWriteData:(int64_t)bytesWritten
totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
NSLog(@"Session %@ download task %@ wrote an additional %lld bytes (total %lld bytes) out of an expected %lld bytes.\n", session, downloadTask, bytesWritten, totalBytesWritten, totalBytesExpectedToWrite);
}
- (void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didResumeAtOffset:(int64_t)fileOffset
expectedTotalBytes:(int64_t)expectedTotalBytes
{
NSLog(@"Session %@ download task %@ resumed at offset %lld bytes out of an expected %lld bytes.\n", session, downloadTask, fileOffset, expectedTotalBytes);
}
- (void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location
{
NSLog(@"Session %@ download task %@ finished downloading to URL %@\n", session, downloadTask, location);
// Perform the completion handler for the current session
self.completionHandlers[session.configuration.identifier]();
// Open the downloaded file for reading
NSError *readError = nil;
NSFileHandle *fileHandle = [NSFileHandle fileHandleForReadingFromURL:location error:readError];
// ...
// Move the file to a new URL
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *cacheDirectory = [[fileManager URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask] firstObject];
NSError *moveError = nil;
if ([fileManager moveItemAtURL:location toURL:cacheDirectory error:moveError]) {
// ...
}
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
print("Session \(session) download task \(downloadTask) wrote an additional \(bytesWritten) bytes (total \(totalBytesWritten) bytes) out of an expected \(totalBytesExpectedToWrite) bytes.\n")
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64) {
print("Session \(session) download task \(downloadTask) resumed at offset \(fileOffset) bytes out of an expected \(expectedTotalBytes) bytes.\n")
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
print("Session \(session) download task \(downloadTask) finished downloading to URL \(location)\n")
// Perform the completion handler for the current session
self.completionHandlers[session.configuration.identifier]()
// Open the downloaded file for reading
if let fileHandle = try? FileHandle(forReadingFrom: location) {
// ...
}
// Move the file to a new URL
let fileManager = FileManager.default()
if let cacheDirectory = fileManager.urlsForDirectory(.cachesDirectory, inDomains: .userDomainMask).first {
do {
try fileManager.moveItem(at: location, to: cacheDirectory)
} catch {
// ...
}
}
}
上传Body内容
App可以使用三种方式为HTTP POST请求提供请求体(body)内容:NSData对象、文件、或者流。通常,app应该:
- 如果app在内存中已经有数据且没有理由清除它时,使用NSData对象。
- 如果你要上传的内容是磁盘中的文件,如果你打算在后台进行传输,或者写入磁盘之后可以清楚内存中相关的数据,请使用文件。
- 如果你正通过网络接收数据,请使用数据流。
无论你选择哪个类型,如果app提供了自定义会话委托,该委托应该实现URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:委托方法类获取上传进度的信息。
另外,如果app使用数据流来提供请求体,它必须提供实现URLSession:task:needNewBodyStream:方法的自定义会话委托。更多详情在Uploading Body Content Using a Stream中描述。
使用NSData对象上传Body内容
想要使用NSData对象上传body内容,app需要调用uploadTaskWithRequest:fromData: 或 uploadTaskWithRequest:fromData:completionHandler:方法来创建上传任务,并通过fromData参数提供请求体数据。
会话对象会基于数据对象的尺寸来计算Content-Length(内容长度)头。
App必须提供给服务器要求的额外头部信息(例如,内容类型),作为URL请求对象的一部分。
代码清单 1-8 从数据示例上传任务
NSURL *textFileURL = [NSURL fileURLWithPath:@"/path/to/file.txt"];
NSData *data = [NSData dataWithContentsOfURL:textFileURL];
NSURL *url = [NSURL URLWithString:@"https://www.example.com/"];
NSMutableURLRequest *mutableRequest = [NSMutableURLRequest requestWithURL:url];
mutableRequest.HTTPMethod = @"POST";
[mutableRequest setValue:[NSString stringWithFormat:@"%lld", data.length] forHTTPHeaderField:@"Content-Length"];
[mutableRequest setValue:@"text/plain" forHTTPHeaderField:@"Content-Type"];
NSURLSessionUploadTask *uploadTask = [defaultSession uploadTaskWithRequest:mutableRequest fromData:data];
[uploadTask resume];
let textFileURL = URL(fileURLWithPath: "/path/to/file.txt")
if let data = try? Data(contentsOf: textFileURL) {
if let url = URL(string: "https://www.example.com/") {
var mutableRequest = MutableURLRequest(url: url)
mutableRequest.httpMethod = "POST"
mutableRequest.setValue("\(data.count)", forHTTPHeaderField: "Content-Length")
mutableRequest.setValue("text/plain", forHTTPHeaderField: "Content-Type")
let uploadTask = defaultSession.uploadTask(with: mutableRequest, from: data)
uploadTask.resume()
}
}
使用文件上传Body内容
想要从文件上传body你容,app应该调用uploadTaskWithRequest:fromFile: 或 uploadTaskWithRequest:fromFile:completionHandler:方法来创建上传任务,并文件的URL。
会话对象给予数据对象的尺寸计算Content-Length(内容长度)头。如果app没有提供Content-Length头,会话也会提供一个。
App可以提供服务器要求的任何额外头部信息,作为请求对象的URL。
清单1-9 从数据流请求上传任务的示例
NSURL *textFileURL = [NSURL fileURLWithPath:@"/path/to/file.txt"];
NSURL *url = [NSURL URLWithString:@"https://www.example.com/"];
NSMutableURLRequest *mutableRequest = [NSMutableURLRequest requestWithURL:url];
mutableRequest.HTTPMethod = @"POST";
NSURLSessionUploadTask *uploadTask = [defaultSession uploadTaskWithRequest:mutableRequest fromFile:textFileURL];
[uploadTask resume];
let textFileURL = URL(fileURLWithPath: "/path/to/file.txt")
if let url = URL(string: "https://www.example.com/") {
var mutableRequest = MutableURLRequest(url: url)
mutableRequest.httpMethod = "POST"
mutableRequest.httpBodyStream = InputStream(fileAtPath: textFileURL.path!)
mutableRequest.setValue("\(data.count)", forHTTPHeaderField: "Content-Length")
mutableRequest.setValue("text/plain", forHTTPHeaderField: "Content-Type")
let uploadTask = defaultSession.uploadTask(with: mutableRequest, from: textFileURL)
uploadTask.resume()
}
使用数据流上传Body内容
想要使用数据流上传body内容,app应该调用uploadTaskWithStreamedRequest:方法类创建上传任务。App提供与body内容数据流相关的请求对象。
App必须提供任何服务器要求的额外信息(例如,内容类型及长度),作为URL请求对象的一部分。
另外,因为会话不一定能够将提供的数据流返回去重新读取数据,所以,当会话必须要重试请求的时候(例如,如果验证失败),app有责任为此提供一个新的数据流。为此,app提供URLSession:task:needNewBodyStream:方法。当该方法被调用的时候,app应该执行所需的任何操作来获取或创建新的body数据流,然后使用该新数据流调用提供的完成处理程序代码块。
注意:因为app必须提供URLSession:task:needNewBodyStream:委托方法,如果它通过数据流提供body,则该技术与系统提供的委托不兼容。
代码清单1-10 从数据流上传任务示例
NSURL *textFileURL = [NSURL fileURLWithPath:@"/path/to/file.txt"];
NSURL *url = [NSURL URLWithString:@"https://www.example.com/"];
NSMutableURLRequest *mutableRequest = [NSMutableURLRequest requestWithURL:url];
mutableRequest.HTTPMethod = @"POST";
mutableRequest.HTTPBodyStream = [NSInputStream inputStreamWithFileAtPath:textFileURL.path];
[mutableRequest setValue:@"text/plain" forHTTPHeaderField:@"Content-Type"];
[mutableRequest setValue:[NSString stringWithFormat:@"%lld", data.length] forHTTPHeaderField:@"Content-Length"];
NSURLSessionUploadTask *uploadTask = [defaultSession uploadTaskWithStreamedRequest:mutableRequest];
[uploadTask resume];
let textFileURL = URL(fileURLWithPath: "/path/to/file.txt")
if let url = URL(string: "https://www.example.com/") {
var mutableRequest = MutableURLRequest(url: url)
mutableRequest.httpMethod = "POST"
let uploadTask = defaultSession.uploadTask(with: mutableRequest, from: textFileURL)
uploadTask.resume()
}
使用下载任务上传文件
想要将下载内容作为上传的body内容,app必须提供NSData对象或body数据流,作为创建下载请求是提供的NSURLRequest对象的一部分。
如果你使用数据流提供数据,app必须提供URLSession:task:needNewBodyStream:委托方法来来在验证失败的情况下提供一个新body数据流。这个方法的功能描述在Uploading Body Content Using a Stream中。
下载任务行为和数据任务很相近,除了给app返回数据的方式之外。
处理身份验证和自定义TLS链验证
如果远程服务器返回一个状态码,表明需要进行身份验证,并且如果该身份验证要求连接级别的挑战(例如SSL客户端证书),那么NSURLSession会调用验证挑战委托方法。
- 对于会话级别挑战——NSURLAuthenticationMethodNTLM, NSURLAuthenticationMethodNegotiate, NSURLAuthenticationMethodClientCertificate, 或 NSURLAuthenticationMethodServerTrust——NSURLSession对象调用会话委托的URLSession:didReceiveChallenge:completionHandler:方法。如果app没有提供会话委托方法,则NSURLSession对象调用任务委托的URLSession:task:didReceiveChallenge:completionHandler:方法来处理挑战。
- 对于非会话级别的挑战(其他所有),NSURLSession对象调用任务委托的URLSession:task:didReceiveChallenge:completionHandler:方法来处理挑战。如果app提供会话委托,并且你需要处理身份验证,你必须处理在任务级别上的验证,或者提供一个明确调用每个会话任务级别的处理程序。该会话委托的URLSession:didReceiveChallenge:completionHandler:方法在非会话级别挑战中不会被调用。
注意:Kerberos验证是显式处理的。
当基于流的上传body任务验证失败的时候,该任务不必返回以及安全的重用数据流。相反,NSURLSession对象调用委托的URLSession:task:needNewBodyStream: 方法来获取新的NSInputStream对象,它为新请求提供body数据。(如果任务的上传body是由文件或NSData对象提供给的,则会话对象不用进行该回调。)
更多关于为NSURLSession编写身份验证委托方法的信息,请阅读Authentication Challenges and TLS Chain Validation。
处理iOS后台活动
如果你在iOS中使用NSURLSession,app会在下载完成的时候自动重启。App的application:handleEventsForBackgroundURLSession:completionHandler:应用程序委托方法负责重建适当的会话、存储一个完成处理程序、以及当该会话调用会话的委托的URLSessionDidFinishEventsForBackgroundURLSession:方法时调用该完成处理程序。
代码清单1-11 提供一个在后台创建并开始下载任务的示例。代码清单1-12和1-13分别显示了这些会话和app委托方法的示例。
代码清单1-11 后台下载任务会话的iOS例子
NSURL *url = [NSURL URLWithString:@"https://www.example.com/"];
NSURLSessionDownloadTask *backgroundDownloadTask = [backgroundSession downloadTaskWithURL:url];
[backgroundDownloadTask resume];
if let url = URL(string: "https://www.example.com/") {
let backgroundDownloadTask = backgroundSession.downloadTask(with: url)
backgroundDownloadTask.resume()
}
代码清单1-12 iOS后台下载的会话委托方法
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
AppDelegate *appDelegate = (AppDelegate *)[[[UIApplication sharedApplication] delegate];
if (appDelegate.backgroundSessionCompletionHandler) {
CompletionHandler completionHandler = appDelegate.backgroundSessionCompletionHandler;
appDelegate.backgroundSessionCompletionHandler = nil;
completionHandler();
}
NSLog(@"All tasks are finished");
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
guard let appDelegate = UIApplication.sharedApplication.delegate as? AppDelegate else {
return
}
if let completionHandler = appDelegate.backgroundSessionCompletionHandler {
appDelegate.backgroundSessionCompletionHandler = nil
completionHandler()
}
}
代码清单1-13 iOS后台下载的app委托方法
@interface AppDelegate : UIResponder <UIApplicationDelegate>
@property (strong, nonatomic) UIWindow *window;
@property (copy) CompletionHandler backgroundSessionCompletionHandler;
@end
@implementation AppDelegate
- (void)application:(UIApplication *)application
handleEventsForBackgroundURLSession:(NSString *)identifier
completionHandler:(void (^)())completionHandler
{
self.backgroundSessionCompletionHandler = completionHandler;
}
@end
class AppDelegate : UIResponder, UIApplicationDelegate {
var window: UIWindow?
var backgroundSessionCompletionHandler: CompletionHandler?
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: () -> Void) {