NSURLSession类和相关类提供了一个用于通过HTTP下载内容的API。该API提供了丰富的代理方法,用于支持身份验证,并使您的应用程序能够在您的应用程序未运行时执行后台下载,或者在iOS中停止应用时执行后台下载。
要使用NSURLSession API,您的应用程序会创建一系列会话,每个会话协调一组相关的数据传输任务。例如,如果您正在编写Web浏览器,则应用程序可能会在每个选项卡或窗口中创建一个会话。在每个会话中,您的应用程序会添加一系列任务,每个任务代表对特定URL的请求(如果原始URL返回HTTP重定向,则为任何后续URL)。
像大多数网络API一样,NSURLSession API非常异步。如果您使用默认的系统提供的代理,则必须提供一个完成处理程序块,当传输成功完成或出现错误时,将返回数据到您的应用程序。或者,如果您提供自己的自定义委托对象,则任务对象将从服务器接收到的数据(或传输完成时进行文件下载)来调用这些代理方法。
注意:完成回调主要用于使用自定义委托的替代方法。 如果使用完成回调的方法创建任务,则不会调用用于响应和数据传递的委托方法。
NSURLSession API提供状态和进度属性,以及向代理提供此信息。 它支持取消,重新启动(恢复)和挂起任务,并提供恢复停止,取消或失败的下载的能力。
了解URL会话概念
会话中任务的行为取决于三个方面:
会话类型(由用于创建它的配置对象的类型决定),
任务类型以及
应用程序在创建任务时是否在前台。
会话类型
NSURLSession API支持三种类型的会话,由用于创建会话的配置对象的类型决定:
默认会话与其他Foundation下载URL的方法类似。 他们使用永久性的基于磁盘的缓存并将凭据存储在用户的钥匙串中。
短暂会话不会将任何数据存储到磁盘中; 所有缓存,凭据存储等都保存在RAM中并与会话相关联。 因此,当您的应用程序使会话无效时,它们将自动清除。
后台会话类似于默认会话,但单独的进程处理所有数据传输。 后台会话有一些额外的限制,在后台传输注意事项中描述。
任务类型
在会话中,NSURLSession类支持三种类型的任务:
数据任务,
下载任务
上传任务。
数据任务使用NSData对象发送和接收数据。 数据任务旨在用于从应用程序到服务器的简短的,经常交互式的请求。 数据任务可以在收到每个数据后一次将数据返回给您的应用程序,或者通过完成处理程序一次性返回到您的应用程序。
下载任务以文件的形式检索数据,并在应用程序未运行时支持后台下载。
上传任务以文件的形式发送数据,并在应用程序未运行时支持后台上传。
后台转移注意事项
NSURLSession类在您的应用程序被暂停时支持后台传输。后台传输仅由使用后台会话配置对象(通过调用backgroundSessionConfiguration :)返回的会话提供)。
使用后台会话,因为实际的传输是由单独的进程执行的,因为重新启动应用程序的进程相对昂贵,所以几个功能不可用,导致以下限制:
会议必须提供一个委托来进行事件传递。 (对于上传和下载,代理人的行为与进程内转移相同)。
只支持HTTP和HTTPS协议(没有自定义协议)。
重定向始终遵循。
仅支持从文件上传任务(从数据对象上传,或程序退出后,流将失败)。
如果在应用程序处于后台时启动后台传输,则配置对象的自由属性将被视为true。
在iOS中,当后台传输完成或需要凭据时,如果您的应用程序不再运行,iOS会在后台自动重新启动应用程序,并调用应用程序:
handleEventsForBackgroundURLSession:completionHandler:方法在您的应用程序的UIApplicationDelegate对象上。
此调用提供导致您的应用启动的会话的标识符。 您的应用程序应该存储该完成处理程序,创建具有相同标识符的后台配置对象,并创建与该配置对象的会话。
新会话将自动重新与正在进行的背景活动相关联。 稍后当会话完成最后一个后台下载任务时,会话委派一个URLSessionDidFinishEventsForBackgroundURLSession:消息。 在该委托方法中,调用主线程上先前存储的完成处理程序,以便操作系统知道再次挂起应用程序是安全的。
在iOS和OS X上,当用户重新启动应用程序时,应用程序应立即创建具有与上次运行应用程序时出现的任何会话相同的标识符的后台配置对象,然后为每个配置对象创建一个会话。 这些新的会话也会自动与正在进行的背景活动重新关联。
注意:您必须每个标识符创建一个会话(在创建配置对象时指定)。 共享相同标识符的多个会话的行为是未定义的。
如果在您的应用程序被暂停时完成任务,那么委托的URLSession:downloadTask:didFinishDownloadingToURL:方法随后被调用,并且与其关联的新下载的文件的URL被调用。
类似地,如果任何任务需要凭据,NSURLSession对象将调用委托的URLSession:task:didReceiveChallenge:completionHandler:method或URLSession:didReceiveChallenge:completionHandler:method。
在网络错误后,URL加载系统会自动重试后台会话中的上传和下载任务。 无需使用可达性API来确定何时重试失败的任务。
有关如何使用NSURLSession进行后台传输的示例,请参阅。
简单后台传输
生命周期和代理互动
根据您使用NSURLSession类的内容,完全了解会话生命周期可能会有所帮助,包括会话如何与委托进行交互,委托调用的顺序,服务器返回重定向时会发生什么, 当您的应用程序恢复失败的下载时,会发生什么,等等。
有关URL会话的生命周期的完整描述,请阅读URL会话的生命周期。
NSCopying行为
会话和任务对象符合NSCopying协议,如下所示:
当您的应用程序复制会话或任务对象时,您会收到相同的对象。
当您的应用程序复制 配置对象时,您将获得可以独立修改的新副本。
示例代理类接口
以下任务部分中的代码片段基于清单1-1所示的类接口。
Listing 1-1 Sample delegate class interface
@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
创建和配置会话
NSURLSession API提供了广泛的配置选项:
以特定于单个会话的方式为缓存,Cookie,凭据和协议提供私有存储支持
认证,绑定到特定请求(任务)或一组请求(会话)
通过URL进行文件上传和下载,从而鼓励数据(文件内容)与元数据(URL和设置)的分离,
配置每个主机的最大连接数
如果整个资源在一定时间内无法下载,则会触发每个资源超时
最小和最大TLS版本支持
自定义代理字典
控制Cookie政策
控制HTTP流行为
因为大多数设置都包含在单独的配置对象中,所以可以重用常用的设置。当您实例化一个会话对象时,请指定以下内容:
管理该会话及其中任务的行为的配置对象
可选地,委托对象在接收到进入的数据时处理传入数据,并处理特定于会话的其他事件及其内的任务,例如服务器认证,确定是否将资源加载请求转换为下载,等等
如果不提供委托,NSURLSession对象使用系统提供的委托。这样,您可以轻松地使用NSURLSession代替使用sendAsynchronousRequest:queue:completionHandler:在NSURLSession上的方便方法的现有代码。
注意:
如果您的应用程序需要执行后台传输,则必须提供自定义代理。
实例化会话对象后,不创建新会话就无法更改配置或委托。
清单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:
a path relative to the ~/Library/Caches directory,*/
NSURLCache *cache = [[NSURLCache alloc] initWithMemoryCapacity:16384 diskCapacity:268435456 diskPath:cachePath];
defaultConfiguration.URLCache = cache;
defaultConfiguration.requestCachePolicy = NSURLRequestUseProtocolCachePolicy;
// Creating sessions
id 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];
除后台配置外,您可以重复使用会话配置对象来创建其他会话。 (您不能重复使用后台会话配置,因为共享相同标识符的两个后台会话对象的行为未定义。)
您还可以随时安全地修改配置对象。 创建会话时,会话在配置对象上执行深层复制,因此修改仅影响新会话,而不影响现有会话。 例如,您可能会为内容创建第二个会话,只有当您处于Wi-Fi连接时才应检索该内容,如清单1-3所示。
使用相同的配置对象创建第二个会话
ephemeralConfiguration.allowsCellularAccess = NO;
NSURLSession *ephemeralSessionWiFiOnly = [NSURLSession sessionWithConfiguration:ephemeralConfiguration delegate:delegate delegateQueue:operationQueue];
使用系统提供的代理获取资源
使用NSURLSession最简单的方法是使用系统提供的代理请求资源。 使用这种方法,您只需在应用程序中只提供两段代码:
基于该对象创建配置对象和会话的代码
完成处理程序例程,在完全收到数据后执行某些操作
使用系统提供的代理,您可以通过每个请求只需一行代码来获取特定的URL。 清单1-4显示了这种简化形式的示例。
注意:
系统提供的代理仅提供有限的网络行为定制。 如果您的应用程式超出了基本网址提取的特殊需求,例如自订验证或背景下载,这种技术是不合适的。 有关必须实现完整委托的完整情况列表,请参阅URL会话的生命周期。
清单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];
使用自定义代理获取数据
如果您正在使用自定义委托来检索数据,则代理必须至少实现以下方法:
URLSession:dataTask:didReceiveData:将请求中的数据提供给您的任务,一次一个。
URLSession:task:didCompleteWithError:向您的任务指出数据已被完全接收。
如果您的应用程序需要在URLSession之后使用数据:dataTask:didReceiveData:method返回,您的代码负责以某种方式存储数据。
例如,Web浏览器可能需要在数据到达时呈现数据,以及之前接收到的任何数据。
为此,可以使用将任务对象映射到用于存储结果的NSMutableData对象的字典,然后在该对象上使用appendData:方法附加新接收的数据。
清单1-5显示了如何创建和启动数据任务。
清单1-5数据任务示例
NSURL *url = [NSURL URLWithString: @"https://www.example.com/"];
NSURLSessionDataTask *dataTask = [defaultSession dataTaskWithURL:url];
[dataTask resume];
下载文件
在高级别,下载文件类似于检索数据。您的应用程序应实现以下委托方法:
URLSession:downloadTask:didFinishDownloadingToURL:将您的应用程序提供给存储下载内容的临时文件的URL。
重要提示:
在此方法返回之前,必须打开文件进行读取或将其移动到永久位置。当此方法返回时,临时文件如果仍然存在于其原始位置,将被删除。
URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:为您的应用程序提供有关下载进度的状态信息。
URLSession:downloadTask:didResumeAtOffset:expectedTotalBytes:告诉您的应用程序尝试恢复以前失败的下载成功。
URLSession:task:didCompleteWithError:告诉您的应用程序下载失败。
如果您在后台会话中安排下载,则当您的应用未运行时,下载将继续。如果您在标准或短暂会话中安排下载,则重新启动应用程序时,下载必须重新开始。
在从服务器传输过程中,如果用户告诉您的应用暂停下载,您的应用可以通过调用cancelByProducingResumeData:方法来取消该任务。之后,您的应用程序可以将返回的简历数据传递给downloadTaskWithResumeData downloadTaskWithResumeData:completionHandler:方法来创建一个继续下载的新的下载任务。
如果传输失败,您的委托URLSession:task:didCompleteWithError:方法被调用一个NSError对象。如果任务可以恢复,该对象的userInfo字典包含NSURLSessionDownloadTaskResumeData键的值;您的应用程序可以将返回的简历数据传递给downloadTaskWithResumeData或downloadTaskWithResumeData:completionHandler:方法来创建重新下载的新的下载任务。
清单1-6提供了下载中等大文件的示例。
列表1-7提供了下载任务委托方法的示例。
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];
- (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]) {
// ...
}
}
上传正文内容
您的应用程序可以通过三种方式为HTTP POST请求提供请求正文内容:作为NSData对象,作为文件或流。一般来说,您的应用应该:
如果您的应用程序已经拥有内存中的数据,并且没有理由处理它,请使用NSData对象。
如果您正在上传的内容存在于磁盘上的文件,如果您正在进行后台传输,或者是将应用程序的好处写入磁盘,以便可以释放与该数据相关联的内存,请使用文件。
如果您通过网络接收数据,请使用流。
无论您选择哪种风格,
如果您的应用程序提供自定义会话代理,
该委托应实现URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:delegate方法
以获取上传进度信息。
此外,如果您的应用程序使用流提供请求正文,
则它必须提供实现URLSession:task的自定义会话委托:needNewBodyStream:方法,在使用流上传身体内容时更详细地介绍。
使用NSData对象上传正文内容
要使用NSData对象上传正文内容,您的应用程序会调用uploadTaskWithRequest:fromData:或uploadTaskWithRequest:fromData:completionHandler:方法来创建上传任务,并通过fromData参数提供请求体数据。
会话对象根据数据对象的大小计算Content-Length头。
您的应用程序必须提供服务器可能需要的任何其他头信息 - 例如内容类型 - 作为URL请求对象的一部分。
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];
使用文件上传正文内容
要从文件中上传正文内容,您的应用程序会调用uploadTaskWithRequest:fromFile:或uploadTaskWithRequest:fromFile:completionHandler:方法来创建上传任务,并提供一个文件URL,任务从该文件URL读取正文内容。
会话对象根据数据对象的大小计算Content-Length头。 如果您的应用程序不提供Content-Type标头的值,则会话还提供一个。
您的应用程序可以提供服务器可能需要的任何额外的头信息,作为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];
使用流上传正文内容
要使用流上传正文内容,您的应用程序将调用uploadTaskWithStreamedRequest:方法来创建上传任务。 您的应用程序提供了一个请求对象与一个关联的流,任务从该对象读取正文内容。
您的应用程序必须提供服务器可能需要的任何其他头信息 - 例如内容类型和长度 - 作为URL请求对象的一部分。
另外,由于会话不一定能够将提供的流倒回重新读取数据,所以在会话必须重试请求的情况下(例如,如果身份验证失败),您的应用程序将负责提供新的流。 为此,您的应用程序提供了一个URLSession:任务:needNewBodyStream:方法。 当调用该方法时,您的应用程序应执行所需的任何操作来获取或创建新的主体流,然后使用新流调用提供的完成处理程序块。
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];
使用下载任务上传文件
要上传下载任务的正文内容,您的应用程序必须提供NSData对象或主体流,作为创建下载请求时提供的NSURLRequest对象的一部分。
如果您使用流提供数据,则您的应用程序必须提供一个URLSession:task:requireNewBodyStream:delegate方法来在身份验证失败的情况下提供新的主体流。 在使用流上传身体内容时进一步描述了此方法。
下载任务的行为就像数据任务,除了将数据返回到您的应用程序的方式之外
处理身份验证和自定义TLS链验证
如果远程服务器返回指示需要认证的状态代码,并且如果该身份验证需要连接级别的质询(例如SSL客户端证书),则NSURLSession会调用身份验证挑战委托方法。
对于会话级别的挑战 - NSURLAuthenticationMethodNTLM,NSURLAuthenticationMethodNegotiate,NSURLAuthenticationMethodClientCertificate或NSURLAuthenticationMethodServerTrust - NSURLSession对象调用会话委托的URLSession:didReceiveChallenge:completionHandler:method。如果您的应用程序不提供会话委托方法,NSURLSession对象将调用任务委托的URLSession:task:didReceiveChallenge:completionHandler:方法来处理挑战。
对于非会话级别的挑战(所有其他),NSURLSession对象调用任务委托的URLSession:task:didReceiveChallenge:completionHandler:方法来处理挑战。如果您的应用程序提供会话委托,并且需要处理身份验证,那么您必须处理任务级别的身份验证,或者提供一个明确调用每个会话处理程序的任务级处理程序。会话代理的URLSession:didReceiveChallenge:completionHandler:方法不是为非会话级别的挑战而调用的。
注意:Kerberos身份验证是透明的。
当具有基于流的上传正文的任务的身份验证失败时,任务不一定能够安全地倒带并重新使用该流。相反,NSURLSession对象调用委托的URLSession:task:needNewBodyStream:delegate方法来获取新的NSInputStream对象,该对象为新请求提供正文数据。 (如果任务的上传正文是从文件或NSData对象提供的,则会话对象不会进行此调用。)
有关为NSURLSession编写身份验证委托方法的更多信息,请阅读身份验证挑战和TLS链验证。
处理iOS背景活动
如果您在iOS中使用NSURLSession,则当下载完成时,您的应用程序将自动重新启动。 您的应用程序的应用程序:handleEventsForBackgroundURLSession:completionHandler:app delegate方法负责重新创建适当的会话,存储完成处理程序,并在会话调用会话委托的URLSessionDidFinishEventsForBackgroundURLSession:方法时调用该处理程序。
清单1-11提供了在后台创建和启动下载任务的示例。 清单1-12和清单1-13分别显示了这些会话和应用程序委托方法的示例。
列表1-11 iOS的会话背景下载任务示例
NSURL *url = [NSURL URLWithString:@"https://www.example.com/"];
NSURLSessionDownloadTask *backgroundDownloadTask = [backgroundSession downloadTaskWithURL:url];
[backgroundDownloadTask resume];
- (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");
}
@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