使用NSURLSession(UsingNSURLSession
)
NSURLSession和其相关的类提供了通过HTTP下载数据的API.该API提供了丰富的代理方法来支持信息身份认证,以及当app未运行时(比如,在iOS中,app挂起状态)的后台下载功能.
为了使用NSURLSession,客户端会创建一系列对话(sessions),每个会话都匹配一组相关的数据传输任务.例如,编码一个web浏览器,客户端可能需要为没一个标签或者窗口创建一个会话.对每个会话,客户端增加一系列任务,每个任务代表了指向一个特定URL得请求(或者HTTP重定向后的URLs).
像大部分网络API一样,NSURLSession是异步的.如果你使用系统默认提供的代理,那么当一个传输成功完成或发生错误时,你必须提供一个处理结果的块(block).如果你使用自定义的代理对象,当从服务器接收到数据后,任务对象会调用自定义的代理方法(对文件下载来说,是当传输完成时调用).
注意:回调block是自定义代理的一个主要备选方案.如果你通过回调block的方式创建了一个任务,那么代理方法将不会再被调用.
NSURLSession API提供了状态和进度属性,我们能获得当前状态以及任务进度值,并能将这些信息传递给代理.它还支持取消,重启(重设),挂起任务,还能从挂起,取消,失败状态重新开始任务.
理解URL Session的概念(Understanding URL Session Concepts)
一个会话中任务的行为取决于3个因素:会话的类型(由一个先前创建的configuration对象的类型决定),任务的类型,以及任务创建时app是否处在前端激活状态.
会话的类型
(Types of Sessions)
NSURLSession
支持三种会话类型(由一个先前创建的configuration对象的类型决定):
1:默认会话类型(
defaultSessionConfiguration
):与其他下载URLs的方法基本类似(NSURLConnection).使用永久磁盘缓存的持久化策略,在用户钥匙串(keychain)中存储认证信息.
2:临时会话类型(
ephemeralSessionConfiguration
):不在磁盘上永久化存储任何信息(缓存,认证),所有信息只保存再与会话关联的内存中,当会话失效时,所有信息都会被清空.
3:后台会话类型(
backgroundSessionConfigurationWithIdentifier
):与默认会话类型大致相似,不同点是有一个单独的线程来处理数据传输.后台会话类型还存在一些额外的限制,参见下文后台数据传输注意事项Background Transfer Considerations.
任务的类型
(Types of Tasks)
一个会话中,NSURLSession支持三种任务类型:加载数据(data tasks),下载数据(download tasks),和上传数据(upload tasks).
1:加载任务类型使用NSData对象来发送和接收数据.使用场景是传输短数据,与服务器交互性强的请求.数据加载能周期性的返回获得的数据(总数据的一部分),或者一次性通过一个下载完成的回调处理整个数据.数据加载并没有将获得的数据存储到文件中,所以不支持后台加载.
2:下载任务类型以文件形式接收数据,支持app处于非运行状态时在后台下载.
3:上传任务类型通常以文件形式发送数据,支持app处于非运行状态时在后台上传.
后台数据传输注意事项
(Background Transfer Considerations)
NSURLSession支持app挂起状态时进行后台数据传输.后台传输只有在使用后台会话配置对象创建的会话中可用.(调用defaultSessionConfiguration()方法获取配置对象)
对后台会话来说,因为真实的数据传输是在一条单独的线程中执行的,并且重新开启一条线程开销相对将大,所以一部分特性将不被支持,导致存在下面一些限制:
会话必须给每一个委托提供一个代理.(对上传和下载类型的会话来说,代理方法与进程内传输一样)
1:只支持HTTP和HTTPS协议(不支持自定义协议)
2:重定向的情况将自动转向重定向后URL.
3:只支持以文件形式上传
4:如果后台数据传输是在app处于后台期间创建的,configuration对象的discretionary属性将被视为true(表明当程序在后台运作时由系统自己选择最佳的网络连接配置,该属性可以节省通过蜂窝连接的带宽).
注意:iOS8 and OS X 10.10之前不支持后台加载数据
app的重启时的表现在iOS和OS X下有些许区别如下:
在iOS中,当一个后台传输完成或者需要进行信息认证时,如果此时app没有运行,iOS将自动在后台重启app并且调用UIApplicationDelegate对象的application:handleEventsForBackgroundURLSession:completionHandler:方法.这个调用提供一个会话标识并导致app重启.app应该存储完成块(completion handler),用同样的会话标识创建一个后台配置(configuration)对象,并通过这个配置对象创建一个会话.这个会话将在后台持续运行.之后,当会话完成了最后一个下载任务,会发送给会话的代理对象一个URLSessionDidFinishEventsForBackgroundURLSession:消息.会话代理对象应当调用并且存储完成块.
在iOS和OS X中,当用户重新运行app时,app应当立即创建一个后台配置对象,使用上一次运行有未完成任务的会话标识(可能包含多个).然后为这些配置对象创建会话.这些新创建的会话同样会在后台持续运行.
注意:必须为每一个会话正确的指定标识(在创建配置对象时指定),多个会话使用同一个标识将会导致定义不明确.
如果有任务在app挂起期间完成,代理对象的URLSession:downloadTask:didFinishDownloadingToURL:,方法将被调用.
类似的
,
如果任务需要进行信息认证
,
会话对象会在调用代理的 URLSession:task:didReceiveChallenge:completionHandler:
方法或者 URLSession:didReceiveChallenge:completionHandler:
方法
.
在后台的上传和下载任务将自动恢复加载在网络出错之后,所以并没有必要使用网络监听相关API确定什么时候恢复失败任务。
对于更多怎样在后台传输数据,可以看
Simple Background Transfer.
生命周期和代理交互
(Life Cycle and Delegate Interaction)
为了更好的使用NSURLSession,最好深入理解会话的生命周期,以及会话是如何与其代理对象进行交互的.比如代理何时被调用,当服务器返回一个重定向URL时的处理,当一个任务下载失败时的处理等等.
完整的会话生命周期描述
,
参见
Life Cycle of a URL Session
.
NSCopying
行为
(NSCopying Behavior)
会话对象和任务对象遵循NSCopying协议如下:
当app复制一个会话或者任务对象时,返回的是这个对象本身(没有创建新对象).
当app复制一个配置对象时,返回一个新创建的配置对象能够独立修改(创建了新对象).
代理类接口(Sample Delegate Class Interface)
Listing 1-1 Sample delegate class interface
#import <Foundation/Foundation.h>
typedef void (^CompletionHandlerType)();
@interface MySessionDelegate : NSObject <NSURLSessionDelegate, NSURLSessionTaskDelegate, NSURLSessionDataDelegate, NSURLSessionDownloadDelegate>
@property NSURLSession *backgroundSession;
@property NSURLSession *defaultSession;
@property NSURLSession *ephemeralSession;
#if TARGET_OS_IPHONE
@property NSMutableDictionary *completionHandlerDictionary;
#endif
- (void) addCompletionHandler: (CompletionHandlerType) handler forSession: (NSString *)identifier;
- (void) callCompletionHandlerForSession: (NSString *)identifier;
@end
创建并配置一个会话(Creatingand
Configuring a Session)
NSURLSession
提供了大量的配置选项
:
1:支持对缓存,cookies,证书的私有存储,以及对单例会话的特定协议
2:关联到一个特定请求(任务),或者一组请求(会话)的认证
3:通过URL上传或下载文件,支持将元数据分割成基于文件内容的短数据
4:配置每个主机的最大连接数
5:当资源无法在一个确定时间内下载时,配置一个超时时间
6:支持安全传输层协议(TLS)的版本区间
7:自定义代理
8:cookie的管理策略
9:HTTP传输管理
大部分的配置都在一个configuration对象中设置,可以重要一些基本设置.初始化一个会话对象(session object)可以进行如下操作:
1:一个configuration对象用来管理会话或任务的行为
2:可选的,一个代理对象用来表示接收数据的进度,会话任务或会话其他事件的进度,比如服务器认证,决定一个加载请求是否可转换为下载请求,等等
3:如果没有指定一个代理,NSURLSession对象将使用系统默认提供的代理.在这种方式中,你可以轻松的使用NSURLSession方法替代已存在的sendAsynchronousRequest:queue:completionHandler:方法
注意:如果app需要在后台进行数据传输,必须使用自定义代理.
在创建一个会话对象之后,不能再去修改它的configuration对象和代理,除了重新创建一个会话.
清单1-2展示了创建默认会话,临时会话和后台会话的示例代码
#if TARGET_OS_IPHONE
self.completionHandlerDictionary = [NSMutableDictionary dictionaryWithCapacity:0];
#endif
/* Create some configuration objects. */
NSURLSessionConfiguration *backgroundConfigObject = [NSURLSessionConfiguration backgroundSessionConfiguration: @"myBackgroundSessionIdentifier"];
NSURLSessionConfiguration *defaultConfigObject = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSessionConfiguration *ephemeralConfigObject = [NSURLSessionConfiguration ephemeralSessionConfiguration];
/* Configure caching behavior for the default session.Note that 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_IPHONE
NSString *cachePath = @"/MyCacheDirectory";
NSArray *myPathList = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString *myPath = [myPathList objectAtIndex:0];
NSString *bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];
NSString *fullCachePath = [[myPath stringByAppendingPathComponent:bundleIdentifier] stringByAppendingPathComponent:cachePath];
NSLog(@"Cache path: %@\n", fullCachePath);
#else
NSString *cachePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"/nsurlsessiondemo.cache"];
NSLog(@"Cache path: %@\n", cachePath);
#endif
NSURLCache *myCache = [[NSURLCache alloc] initWithMemoryCapacity: 16384 diskCapacity: 268435456 diskPath: cachePath];
defaultConfigObject.URLCache = myCache;
defaultConfigObject.requestCachePolicy = NSURLRequestUseProtocolCachePolicy;
/* Create a session for each configurations. */
self.defaultSession = [NSURLSession sessionWithConfiguration: defaultConfigObject delegate: self delegateQueue: [NSOperationQueue mainQueue]];
self.backgroundSession = [NSURLSession sessionWithConfiguration: backgroundConfigObject delegate: self delegateQueue: [NSOperationQueue mainQueue]];
self.ephemeralSession = [NSURLSession sessionWithConfiguration: ephemeralConfigObject delegate: self delegateQueue: [NSOperationQueue mainQueue]];
除了后台配置对象(background configurations),我们可以重用配置对象来创建其他的会话.(不能重用后台配置对象是因为两个后台会话不能使用相同的标识符identifier)
你可以在任何时间安全的修改一个configuration对象.因为当创建一个会话时,configuration对象的传递是由深拷贝实现的,所以修改只会影响之后新创建的会话,不会对已存在的会话造成影响.例如,你可能想创建另一个只有在WiFi环境下才能重连数据的会话,如1-3中所示
:
Listing 1-3 Creating a second session with the same configuration object
ephemeralConfigObject.allowsCellularAccess = NO;
// ...
NSURLSession *ephemeralSessionWiFiOnly = [NSURLSession sessionWithConfiguration: ephemeralConfigObject delegate: self delegateQueue: [NSOperationQueue mainQueue]];
使用系统提供的代理抓取资源
(Fetching Resources Using System-Provided Delegates)
最简单直接的使用NSURLSession的方法是用来替换掉之前的sendAsynchronousRequest:queue:completionHandler:方法.使用该操作,我们需要在app中实现两处代码:
1:创建configuration对象,以及一个基于该configuration对象的会话对象
2:一个完成处理程序来处理数据接收完成后要做的事情
使用系统提供的代理,你可以每个请求只用一行代码来抓取特定URL.清单1-4示例了最简单的实现.
注意:系统提供的代理仅仅实现了限制了定制网络行为.如果app的需求超出了基本的URL加载,比如自定义认证或者数据后台下载,那么就需要实现一个完整的代理,参见URL Session的生命周期
.
Listing 1-4 Requesting a resource using system-provided delegates
NSURLSession *delegateFreeSession = [NSURLSession sessionWithConfiguration: defaultConfigObject delegate: nil delegateQueue: [NSOperationQueue mainQueue]];
[[delegateFreeSession dataTaskWithURL: [NSURL URLWithString: @"http://www.example.com/"]
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];
使用自定义代理获取数据
(Fetching Data Using a Custom Delegate)
如果使用自定义代理获取数据,那么代理必须实现下面两个方法:
1:URLSession:dataTask:didReceiveData:提供了任务请求返回的数据,周期性的返回数据块
2:URLSession:task:didCompleteWithError:表明数据是否全部完成接收
如果app需要在URLSession:dataTask:didReceiveData:方法返回之后使用数据,必须用代码实现数据存储.
例如,一个web浏览器可能需要根据之前接收的数据来渲染当前接收的数据.要实现这个功能可以使用一个NSMutableData对象来存储结果数据,然后使用appendData: 来将当前接收的数据拼接到之前接收到的数据中
.
Listing 1-5 shows how you create and start a data task. Data task example
NSURL *url = [NSURL URLWithString: @"http://www.example.com/"];
NSURLSessionDataTask *dataTask = [self.defaultSession dataTaskWithURL: url];
[dataTask resume];
下载文件
(Downloading Files)
某种程序上,下载文件和接收数据类似.app应当实现以下的代理方法:
1:URLSession:downloadTask:didFinishDownloadingToURL:提供app下载内容的临时存储目录.
注意:在这个方法返回之前,必须打开文件来进行读取或者将下载内容移动到一个永久目录.当方法返回后,临时文件将会被删除.
2:URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:
提供了下载进度的状态信息.
3:URLSession:downloadTask:didResumeAtOffset:expectedTotalBytes:告诉app尝试恢复之前失败的下载.
4:URLSession:task:didCompleteWithError::告诉app下载失败.
如果将下载任务安排在后台会话中,在app非运行期间下载行为仍将继续.如果将下载任务安排在系统默认会话或者临时会话中,当app重新启动时,下载也将重新开始.
在跟服务器传输数据期间,如果用户进行了暂停操作,app可以调用cancelByProducingResumeData:方法取消任务.然后,app可以将已传输的数据作为参数传递给downloadTaskWithResumeData:或者downloadTaskWithResumeData:completionHandler:来创建一个新的下载任务继续下载.
清单1-6示例了一个大文件的下载.清单1-7示例了下载任务的代理方法
.
Listing 1-6 Download task example
NSURL *url = [NSURL URLWithString: @"https://developer.apple.com/library/ios/documentation/Cocoa/Reference/"
"Foundation/ObjC_classic/FoundationObjC.pdf"];
NSURLSessionDownloadTask *downloadTask = [self.backgroundSession downloadTaskWithURL: url];
[downloadTask resume];
Listing 1-7 Delegate methods for download tasks
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
{
NSLog(@"Session %@ download task %@ finished downloading to URL %@\n",
session, downloadTask, location);
#if 0
/* Workaround */
[self callCompletionHandlerForSession:session.configuration.identifier];
#endif
#define READ_THE_FILE 0
#if READ_THE_FILE
/* Open the newly downloaded file for reading. */
NSError *err = nil;
NSFileHandle *fh = [NSFileHandle fileHandleForReadingFromURL:location
error: &err];
/* Store this file handle somewhere, and read data from it. */
#else
NSError *err = nil;
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *cacheDir = [[NSHomeDirectory()
stringByAppendingPathComponent:@"Library"]
stringByAppendingPathComponent:@"Caches"];
NSURL *cacheDirURL = [NSURL fileURLWithPath:cacheDir];
if ([fileManager moveItemAtURL:location
toURL:cacheDirURL
error: &err]) {
/* Store some reference to the new URL */
} else {
/* Handle the error. */
}
#endif
}
-(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);
}
上传数据内容
(Uploading Body Content)
app能通过三种方式通过提供HTTP POST请求体内容:NSData对象,文件和流.通常情况下,appy应该:
1:使用一个NSData对象,如果内存中已经存在相应数据,并且数据不会被无理由的销毁.
2:使用文件形式,如果要上传的内容是通过文件形式存储在硬盘中的,如果是后台传输,或者APP有利于将要上传的内容写入文件,这样可以释放内存相关的数据。
3:使用流,如果是从网络接收数据,或者转化已存在的提供了流的NSURLConnection代码.
无论你选择了哪种方法,如果app提供了自定义代理,都应该实现URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:
方法来获取上传进度信息.
此外,如果app使用流作为请求体,还必须提供一个自定义会话代理实现URLSession:task:needNewBodyStream:方法,详细描述在通过流上传数据Uploading Body Content Using a Stream.
使用NSData对象上传(Uploading Body Content Using an NSData Object)
使用NSData对象上传数据,app需要调用 uploadTaskWithRequest:fromData:
或者 uploadTaskWithRequest:fromData:completionHandler:
来创建一个上传任务,将要上传的NSData对象传递给fromData参数.
会话对象根据NSData对象计算内容长度,赋值给请求头的Content-Length.app必须在URL request对象中提供服务器可能需要的请求头信息-例如:content type.
使用文件形式上传(ploading Body Content Using a File)
使用文件形式上传,app需要调用
uploadTaskWithRequest:fromFile:
或者 uploadTaskWithRequest:fromFile:completionHandler:
方法来创建一个上传任务,以及提供一个文件路径来读取内容.
会话对象自动计算Content-Length,如果app没有提供Content-Type,会话对象将自动生成一个.app还要在URL request对象中提供服务器可能需要的请求头信息.
使用流形式上传(Uploading Body Content Using a Stream)
使用流来上传信息,app需要调用 uploadTaskWithStreamedRequest:
方法来创建一个上传任务.app提供一个与stream相关的request对象读取请求体内容.app必须在URL request对象中提供服务器可能需要的请求头信息,比如content-type和content-length.
此外,因为会话对象不能保证必定能从提供的流中读取数据,所以app需要提供一个新的流以便会话重新进行请求(比如,认证失败).app需要实现URLSession:task:needNewBodyStream:方法.当这个方法被调用时,app需要取得或者创建一个新的流,然后调用提供的完成处理块.
注意:因为app必须实现 URLSession:task:needNewBodyStream:
方法,所以这种形式不支持使用系统默认的代理.
使用下载任务来上传文件(Uploading a File Using a Download Task)
当下载任务创建时,app需要提供一个NSData对象或者一个流作为NSURLRequest对象的参数.
如果使用数据流,app需要实现 URLSession:task:needNewBodyStream:
方法来处理认证失败的情况.详细描述在通过流上传数据Uploading Body Content Using a Stream.
处理认证和安全传输确认(Handling Authenticationand
Custom TLS Chain Validation)
如果远程服务器返回一个状态值表明需要进行认证或者认证需要特定的环境(例如一个SSL客户端证书),NSURLSession调用会调用一个认证相关的代理方法.
1:会话级别:NSURLAuthenticationMethodNTLM
, NSURLAuthenticationMethodNegotiate
, NSURLAuthenticationMethodClientCertificate
, or NSURLAuthenticationMethodServerTrust
,会话对象调用会话代理方法URLSession:didReceiveChallenge:completionHandler:
.如果app没有提供会话代理,会话对象调用任务得代理方法 URLSession:task:didReceiveChallenge:completionHandler:
处理.
2:非会话级别:NSURLSession对象调用会话代理方法URLSession:task:didReceiveChallenge:completionHandler:.如果app提供了会话代理,而且app需要处理认证,那么你必须在任务级别进行处理.在非会话级别上,URLSession:didReceiveChallenge:completionHandler:
不会被调用.
当以流的形式上传,认证失败,任务将不再在重要该流进行上传。相反,NSURLSession对象将告诉代理URLSession:task:needNewBodyStream:方法获取应该信得NSInputStream 对象提供数据进行新的请求。对于更多信息关于NSURLSession的授权代理方法,可以看Authentication Challenges and TLS Chain Validation.
处理iOS后台活动
(Handling iOS Background Activity)
在iOS中使用NSURLSession,当一个下载任务完成时,app将会自动重启.app代理方法application:handleEventsForBackgroundURLSession:completionHandler:负责重建合适的会话,存储完成处理块,并在会话对象调用会话代理的URLSessionDidFinishEventsForBackgroundURLSession:方法时调用完成处理块.
清单1-8示例了这些会话代理方法
Listing 1-8 Session delegate methods for iOS background downloads
#if TARGET_OS_IPHONE
-(void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session
{
NSLog(@"Background URL session %@ finished events.\n", session);
if (session.configuration.identifier)
[self callCompletionHandlerForSession: session.configuration.identifier];
}
- (void) addCompletionHandler: (CompletionHandlerType) handler forSession: (NSString *)identifier
{
if ([ self.completionHandlerDictionary objectForKey: identifier]) {
NSLog(@"Error: Got multiple handlers for a single session identifier. This should not happen.\n");
}
[ self.completionHandlerDictionary setObject:handler forKey: identifier];
}
- (void) callCompletionHandlerForSession: (NSString *)identifier
{
CompletionHandlerType handler = [self.completionHandlerDictionary objectForKey: identifier];
if (handler) {
[self.completionHandlerDictionary removeObjectForKey: identifier];
NSLog(@"Calling completion handler.\n");
handler();
}
}
#endif
清单1-9示例了APP代理方法:
Listing 1-9 App delegate methods for iOS background downloads
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler
{
NSURLSessionConfiguration *backgroundConfigObject = [NSURLSessionConfiguration backgroundSessionConfiguration: identifier];
NSURLSession *backgroundSession = [NSURLSession sessionWithConfiguration: backgroundConfigObject delegate: self.mySessionDelegate delegateQueue: [NSOperationQueue mainQueue]];
NSLog(@"Rejoining session %@\n", identifier);
[ self.mySessionDelegate addCompletionHandler: completionHandler forSession: identifier];
}