Using NSURLSession
NSURLSession类以及相关的类提供了通过HTTP协议下载数据内容的方法。NSURLSession能够提供丰富的代理方法去支持认证和在程序中断或者在后台的情况下去下载内容的能力。
为了使用NSURLSession你的程序创建了一系列的session,每一个session要自己去协调自己的数据和执行的任务,如果你编写了一个浏览器,你的应用需要给没一个tab(选项卡)创建一个session,通过session技术,你的程序可以和URL之间进行丰富的操作。(即便是原始URL,会返回重定向的URL)。
就想很多的网络API一样,NSURLSession是异步执行的。如果你使用默认的模式,系统提供代理,你必须提供相应的的block去处理获取数据成功,或者是出现了错误,
笔记:完成后的回调主要还是选择使用自定义的delegate来实现。如果创建了一个任务使用的时候,使用了自己的方法去处理回调,这个时候delegate将不会再去响应。
Understanding URL Session Concepts
任务能在session中执行,依赖于三个事情:1session的类型(取决于使用configuration 对象去创建它),2任务的类型,3创建session的时候程序是否在前端
Type of session
NSURLSession支持三种类型的任务:data 任务、下载任务、上传任务
- data任务,就是通过NSData去传送数据。这种任务是经常需要与服务器进行交互的,他们通过一小片的data来不断的获取数据。这种任务都是很短暂的,而且不会储存任务到文件,所以这种任务不支持后台session。
- 下载任务是以文件的方式来获取数据,并且支持在后台进行工作(暂停,关闭)
- 上传任务,发送数据给服务器,并且支持在后台进行工作(暂停,关闭)
Backgound Transfer Considerations
NSURLSession类支持后台传输数据,即便是在程序已经挂起,暂停,终止的情况。仅仅只有session能提供后台传输的功能。(使用 backgouundSessionConfiuraton:)
- 后台的session,由于真正的传输操作都是由另一个线程来控制的,又因为重启程序的进程是很大的消耗,所以还是一些功能是不能用的,有一些限制:
- session必须提供一个事件代理。(上传和下载,代理的和在程序里面是使用是一样的)
- 只有http和https协议是支持的(没有自定义的协议)
- 仅仅上传和下载任务支持
- 重定向默认是执行的
- 如果后台传输的session已经建立,configuration对象的discretionary属性是默认被看做为ture的。
在ios和osx中,程序重启还是有一些区别的,如下:
在ios中,当你后台传输完成或者说取药证书的时候,然而此时你的程序已经不运行了,ios 会自动的在后台重启的这个程序并且调用UIApplicationDalegate中的application:handleEventsForBackgroundURLSession:completionHandler:这个方法。这个方法提供了session的标示符并且重启这个程序。你的程序需要存储完成时候的handler,创建一个后台的configration object,使用一个标示符,session是使用configration来创建的。这个新的session时自动重连到正在执行的后台的activity的。然后,让sesion完成了后台的下载任务,它提供一个session的代理URLSessionDidFinishEventsForBackgroundURLSession: 信息,你的session delegate应该呼叫那个储存的处理方法。
在ios和osx中,当用户重启程序的时候,程序都应该立即的通过相同的identifier创建 backgouund configration对象和session最后未完成的时候的任务。然后接管这些未完成的任务和标示符,新的标示符就会自动的像原来的那个session一样工作
如果你任务完成的时候,程序已经停止。这时候代理方法URLSession:downloadTask:didFinishDownloadingToURL: 会把task和url和新下载的文件关联起来。
如果程序需要证书,NSURLSession对象会调用URLSession:task:didReceiveChallenge:completionHandler: 方法或者URLSession:didReceiveChallenge:completionHandler:
。关于如何使用NSURLSession进行后台下载,请参考 Simple Background Transfer。
Life Cycle and Delegate Interaction
通过你使用NSURLSession所做的事情,对于了解NSURLSession的生命周期是很有帮助的,比如代理方法的使用,什么时间方法会被调用,什么时间程序下载文件失败,等等。关于完成的URL Session的生存的描述,请阅读“Life Cycle of a URL Session.”
NSCopying Brehavior
- 程序copy你哥ssession或者task对象,你获得一个同样的对象。
- 程序copy一个configration 对象,你获得一个新的对象可以独立的使用。
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
- Creating and Configuring a Session
- NSURLSession API提供了多个可以配置的属性:
- 可以给单一的session制定特殊的 私有的cache,cookies,证书,以及协议的管理
- 认证,可以绑定一个或者多个request
- 通过URL上传和下载任务,尽量通过metadata去区分
- 可以设定一个主机(host)最多可以有多少链接
- 如果整个资源在某些时间不能被下载,单个的资源的超时也将会被触发
- 设置TLS(传输层协议版本)最大和最小的支持版本
- 何止代理的字典
- 控制cookie的策略
- shi控制HTTP管道行为(pipelining behavior)
由于大部分的NSURLSession的设置都是通过Configration 对象来的,你可以重新使用这个配置对象当你实例化一个session的对象的时候,请参考下面:
- configration对象配置了大部分的session的行为特点
- (可选的)一个代理对象去处理接受的数据和需要控制的时间,比如服务器认证,资源是否需要转入下载等等。如果你没有提供一个代理,NSURLSession对象使用系统童工的代理,换句话说,你可以直接使用系统提供的代理的方法。比如
sendAsynchronousRequest:queue:completionHandler:
当你创建了一个session的时候模拟就不能在改变他的configration,除非你再去创建一个新的session。
Creating and configuring sessions
#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]];
看到上面的例子,除了backgouund的session是不能重用defaultconfig的其他的可以重用。backgouund不能重用是因为两个backgouund session重用一个congfig,那样会导致indentifier是未定义的。
你可以在任何时间很安全的区修噶configration对象,当你你创建一个session的时候,他的configration是深复制的,此时修改configration是不会影响到已经创建的session的,但是新创建的session是会被影响额。
Creating a second session with the same configuration object
ephemeralConfigObject.allowsCellularAccess = YES; // ... NSURLSession *ephemeralSessionWiFiOnly = [NSURLSession sessionWithConfiguration: ephemeralConfigObject delegate: self delegateQueue: [NSOperationQueue mainQueue]];
Fetching Resources Using System-Provided Delegate
使用简单的使用NSURLSession是通过,sendAsynchronousRequest:queue:completionHandler: 这个方法,你仅仅需要两段代码就可以了。
一、创建一个configration对象和session对象
二、一个完成控制端的代码
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];
Feching Data Using a Custom Delegate
如果你使用自定义的delegate来取获取数据,那么这个delegate必须要实现下面的两个方法:
-
URLSession:dataTask:didReceiveData:提供请求的接收到额数据 - URLSession:task:didCompleteWithError:表明你的task接受的数据是否已经全部都接收到了
如果你的程序需要在URLSession:dataTask:didReceiveData:返回后使用数据,那么你的代码必须要通过某种方式把数据存储起来。
举个例子,一个浏览器需要把已经收到的数据和将要收到的数据进行处理。它会使用字典去匹配收到的NSMutableData对象,然后使用appendData方法把去把新数据加到老的数据。
shows how you create and start a data task.
NSURL *url = [NSURL URLWithString: @"http://www.example.com/"]; NSURLSessionDataTask *dataTask = [self.defaultSession dataTaskWithURL: url]; [dataTask resume];
Downloading Files
在高层的操作来说,下载文件和获取数据是相似的。程序里面需要实现下面的方法:
- URLSession:downloadTask:didFinishDownloadingToURL:提供了一个临时的地方去储存下载到的内容。当这个方法返回之后,临时地址的文件将会被删除,所以你需要在返回之前,把数据移动到持久的存储的地方
-
URLSession:downloadTask:didResumeAtOffset:expectedTotalBytes:这个方法告诉程序之前下载失败的任务,被重新下载成功了 URLSession:task:didCompleteWithError:宣告程序下载失败
如果你安排一个下载的任务,这个session是backgouund session,这个下载一直会执行,不管这个程序是否还在运行。如果这个任务是标准或者临时的 session,这个下载任务必须都是要在程序重新开始之后,才能执行。
如果你的程序正在和服务器进行传输,这时候用户想暂停下载,你的程序可以使用这个方法cancelByProducingResumeData: 可以让程序取消任务的同时,也把收到数据存下来。等你次啊次需要恢复下载的时候,你可以使用
downloadTaskWithResumeData: ordownloadTaskWithResumeData:completionHandler: 这两个方法,去创建一个新的下载的任务,去继续进行下载。
如果传输失败,那么delegate的 URLSession:task:didCompleteWithError:这个方法会执行,而且会使用NSError对象。如果这个任务是可以重新执行的,那么你userinfo的字典里面会有NSURLSessionDownloadTaskResumeData关键字。你的应用需要使用reachability APIs 去决定什么时间去重试,这个时候你可以使用downloadTaskWithResumeData: ordownloadTaskWithResumeData:completionHandler:去重新创建新任务进行继续下载
1-6 provides an example of downloading a moderately large file. Listing
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];
1-7provides an example of download task delegate methods.
-(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
程序中上传HTTP的 POST的request的body部分数据,可以有三种方法,NSData、file、stream。一般可以这样处理:
- 使用NSData对象。如果你的数据已经在内存中,并且不需要任何的设置,那么请使用NSData。
- 使用文件对象。如果你的文件放在磁盘上,而且你也方便去进行读写,那么使用文件去上传,对于内寸的使用也是有好处的
- 使用stream上传。如果你从不断的从网络上去收到数据或者把NSURLConnection请求的代码可以转为流进行上传。
无论你选择那种方式,只要是你选择了自定义的session delegate来处理,你就需要完成URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:方法去包含上传进度的信息。
另外,如果你的程序把request弄成了stream各式,那他必须提供一个自定义的session delegate来实现URLSession:task:needNewBodyStream: 这个方法,如果想了解更多,请阅读
“Uploading Body Content Using a Stream.”
Uploading Body Content Using an NSData Object
为了上传NSData类型的数据,程序里面需要去调用uploadTaskWithRequest:fromData: oruploadTaskWithRequest:fromData:completionHandler: 这个两个之一的方法去创建一个上传的任务,并且通过这些方法里面的Fromdata的参数来提供request body。这个session会根据body的size来计算header中content-length的值。
你的程序需要提供而外的请求的Header的数据,以便于服务器更好的接受,比如content类型等。
Uploading Body Content Using a File
为了以文件的方式来上传数据你需要使用下面的之一的方法,uploadTaskWithRequest:fromFile: oruploadTaskWithRequest:fromFile:completionHandler:来创建任务,需要提供一个URL,以便于能够获得请求的内容。
这个session对象会自动计算content-length的值。如果你的程序没有提供content-type的header,呢么session也会提供一个。你也可以根据URL Request的头部需求来提供额外的header的信息
Uploading Body Content Using a stream
为了使用stream来上传数据,你需要使用uploadTaskWithStreamedRequest:方法来创建任务。程序会通过stream来获取数据。
你的程序,需要提供必须的header信息,比如服务器需要的content type和length。就URL request对象一样。
因为stream上传是不能倒退的,所以你如果有新的时间或者需求都需要重新发一个request。为了实现这个ios提供了URLSession:task:needNewBodyStream: 这个方法,当这个方法调用的时候你需要处理创建一个新的body stream所有的工作,并且需要去提供新stream完成handler block。
注意:因为你的程序必须提供URLSession:task:needNewBodyStream:的delegate只要你用了stream,这个技术和系统提供的delegate时不相容的。
Uploading a File Using a Download Task
为一个download task上传body,如果你的程序里面使用NSData或者body stream来作为NSURLRequest对象,去创建一个download request。
f you provide the data using a stream, your app must provide aURLSession:task:needNewBodyStream: delegate method to provide a new body stream in the event of an authentication failure. This method is described further in“Uploading Body Content Using a Stream.”
The download task behaves just like a data task except for the way in which the data is returned to your app.
Handling Authentication and Custom TLS Chain Validation(管理认证和定义传输层chain认证)
- 如果远端的服务器返回了一个状态码,表明需要认证或者是需要一个connection-level challenge,NSURLSession呼叫认证challenge代理方法。
Session-level challenges
NSURLAuthenticationMethodNTLM,
NSURLAuthenticationMethodNegotiate,
NSURLAuthenticationMethodClientCertificate, orNSURLAuthenticationMethodServerTrust
NSURLSession会调用URLSession:didReceiveChallenge:completionHandler:这个方法。如果程序没有提供一个session delegate方法,NSURLSession对象会呼叫task的代理URLSession:task:didReceiveChallenge:completionHandler: 来处理challenge。
对于非session-levelchallenges,NSURLSession对象,会呼叫代理的URLSession:task:didReceiveChallenge:completionHandler:去处理chanllenge。如果程序提供了以后各session delegate并且你需要去处理认证,你需要处理task-level的认证或者提供一个每哥session处理都指定的task-level的处理。session的代理 URLSession:didReceiveChallenge:completionHandler:方法将不会来处理non-session-level的challenges。
Note: Kerberos authentication is handled transparently.
当一个使用stream body去发送的认证task,失败后,你不能在使用这个stream,因为stream是不能倒带的,你需要使用URLSession:task:needNewBodyStream:去新建一个NSInputStream去提欧诺个一个body体。
关于更多的认证的代理的方法请阅读“Authentication Challenges and TLS Chain Validation.”
Handling iOS Background Activity
如果你使用NSURLSession,你的app会自动的重启,当下载的任务完成的时候。程序的session开始呼叫你delegate的URLSessionDidFinishEventsForBackgroundURLSession:
你的程序会调用
application:handleEventsForBackgroundURLSession:completionHandler:
去重新建立一个合适的session,完成下载任务,并且包含一个完成hander去处理。
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
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]; }