使用NSURLSession
NSURLSession
及其相关的类提供了通过HTTP来下载内容的API。这些API提供了丰富的代理方法来支持授权,以及在app没有运行,或者在iOS中,app被挂起时,来执行后台下载。
使用NSURLSession
API,需要创建一系列的session,每个session协调一组相关的数据传送任务。例如,如果你正在写一个浏览器,你可能会为每个tab或者window创建一个session。
与大多数网络APIs类似,NSURLSession
API也是高度异步的。如果你使用默认的,系统提供的代理方法,当传输成功或者出错时,必须提供一个完成处理的回调block。另一种情况是,你提供了自定义的代理对象,当从服务器接收到数据时(对于文件下载,文件完成传送),任务对象会调用这些代理方法。
注意:完成的回调方法主要是用来替代自定义的代理的。如果你创建的任务使用了回调,那么响应的代理和数据传输的代理方法,将不会调用
NSURLSession
API提供了状态和进度属性,并把这些信息传递给代理。它支持取消、 重新启动 (恢复),和挂起任务,并提供了能够恢复暂停、 取消,或者失败的下载的能力。
理解URL Session概念
session中任务的行为取决于三件东西:session的类型(由创建它的配置对象决定)、task的类型,和当创建task时,app是否在前台。
sessions 的类型
NSURLSession
API支持3种类型的session,是由创建session时用到的配置对象决定的:
- Default sessions 与其他的下载URLs的Foundation方法相似。它使用持久性的基于磁盘的缓存,并将凭据保存在用户的钥匙串中。
- Ephemeral sessions不存储任何数据到磁盘,所有缓存、 凭据存储等等都是保存在 RAM 并且和session绑定。因此,当您的应用程序作废session时,它们被自动清除。
- Background sessions类似于默认会话,除了有一个单独的进程处理所有的数据传输。Background sessions有一些额外的限制,在Background Transfer Considerations 中描述。
Tasks的类型
在一个session中,NSURLSession
支持三种类型的tasks:data tasks,download tasks,和 upload tasks。
- Data tasks使用
NSData
对象发送和接收数据。Data tasks是为短的,通常是从您的应用程序到服务器的交互式请求而准备的。Data tasks可以在接收到每个数据片段后返回数据,也可以一次全部完成后通过一个完成后的handler来返回数据。因为data task不将数据存储到一个文件,它们不支持Background sessions。 - Download tasks以文件的形式检索数据,并支持后台下载当应用程序未运行的时候。
- Upload tasks发送数据 (通常以文件的形式),并支持后台上传当应用程序未运行的时候。
后台传输注意事项
NSURLSession
支持后台传输当app被挂起的时候。后台传输仅仅由使用background session配置对象创建的sessions来提供。(调用 backgroundSessionConfiguration 返回)。
background session由于实际传送是由一个单独的进程来执行的,而且由于重新启动您的应用程序的过程是相对昂贵的,所以导致一些功能将不可用,导致以下的限制:
- session必须为事件交付提供一个代理。(对于上传和下载,代理的行为和处理传送相同。)
- 只支持HTTP和HTTPS协议。(不支持自定义协议)
- 只支持upload和download task。(不支持data task)
- 总是有重定向。
- 如果app在后台时初始化后台传送,配置对象的discretionary属性被视为ture。
app重新加载的方式在iOS和OS X中有轻微的不同。
在iOS中,当后台传输完成或者需要credential的时候,如果你的app不再运行,iOS会自动在后台加载app,并调用app的 UIApplicationDelegate
对象的 application:handleEventsForBackgroundURLSession:completionHandler:
方法。这个调用方法提供了加载你app的session的标示符。你的app应该保存这个完成的handler,使用这个session的标示符创建一个配置对象,使用这个配置对象创建一个session。这个新的session会自动和正在后台运行的活动联系在一起。然后,当session完成了后台的下载任务,它给session的代理发送URLSessionDidFinishEventsForBackgroundURLSession:
消息。然后,你的session的代理应该调用保存的那个completion handler。
在iOS和OS X中,当用户重新加载你的app时,app应该立即使用上次app运行时未完成的任务session的标示符创建配置对象,然后为每个配置对象创建一个session。这个新的session会自动和正在后台运行的活动联系在一起。
注意:你必须为每个标示符创建唯一的一个session (特别是你创建配置对象的时候) 。多个session共享相同的标识符的行为是未定义的。
当app被挂起的时候,任何任务完成都会调用URLSession:downloadTask:didFinishDownloadingToURL: 方法,并会带上task和与之相关的新下载文件的URL。
类似的,如果task需要credentials,NSURLSession
对象会调用代理的 URLSession:task:didReceiveChallenge:completionHandler: 或者 URLSession:didReceiveChallenge:completionHandler: 方法。
怎样使用NSURLSession
后台传输的例子,参见Simple Background Transfer.
生命周期和代理交互
取决于你如何运用NSURLSession
类,了解session的生命周期可以会很有帮助,这包括session是如何和代理相互作用的,哪个代理被调用,当服务器返回一个重定向时会发生什么,当你的app回复一个失败的下载时会发生什么,等等。
URL session生命周期的完整描述,请阅读 Life Cycle of a URL Session.
NSCopying的行为
Session和task对象都遵守 NSCopying
协议,如下:
- 当app复制一个session或者task对象时,会返回同一个对象。
- 当app复制一个配置对象时,会返回一个新的对象,这样的话你能独立的修改。
代理类接口的例子
下面的task部分的代码片段基于Listing 1-1 所示的类接口。
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
创建和配置Session
由于大多数的设置都被包含在一个单独的配置对象中,你能重用这些设置。在实例化一个session对象时,需指定如下的东西:
- 一个管理session的行为和其中的任务的配置对象
- 可选,一个委托对象以处理传入的数据,和处理特定的session以及其中的任务的事件,如服务器身份验证,确定一个资源任务加载请求是否应该转换成一个下载,等等
如果不提供代理对象,NSURLSession
会使用系统提供的代理对象。这样的话,你可以使用NSURLSession
的sendAsynchronousRequest:queue:completionHandler:方法取代现有的方法。
注意:如果你的app需要执行后台的传输,必须提供一个自定义的代理。
在实例化一个session对象后,没有创建一个新的session,你不能改变配置or代理对象。
下面的例子,Listing 1-2展示了如何创建normal, ephemeral, and background sessions.
Listing 1-2 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]];
除了后台配置,您可以重用session配置对象来创建附加的session。(你不能重用后台session配置,因为共享相同的标识符的两个背景session对象的行为是未定义的。)
你可以安全的在任何时间修改配置对象。当你创建一个session时,session对配置对象执行深复制,所以修改只会影响新的session,不会影响已经存在的session。例如,你可能会为检索的内容创建第二个session,当你在 有Wi-Fi 连接的时候。如List 1-3 所示。
Listing 1-3 Creating a second session with the same configuration object
ephemeralConfigObject.allowsCellularAccess = YES;
// ...
NSURLSession *ephemeralSessionWiFiOnly = [NSURLSession sessionWithConfiguration: ephemeralConfigObject delegate: self delegateQueue: [NSOperationQueue mainQueue]];
使用系统提供的对象来获取数据
使用此种方法,app需提供了两段代码:
- 创建一个配置对象,和基于此配置对象的session。
- 一个接收完数据后的completion handler
Listing 1-4 使用系统提供的代理来请求数据
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];
使用自定代理
使用自定义代理,至少需要实现如下的方法:
在URLSession:dataTask:didReceiveData:
中可能需要使用NSMutableData
的appendData:
方法来保存数据。
Listing 1-5 展示如何创建和启动一个data task
Listing 1-5 Data task example
NSURL *url = [NSURL URLWithString: @"http://www.example.com/"];
NSURLSessionDataTask *dataTask = [self.defaultSession dataTaskWithURL: url];
[dataTask resume];
下载文件
在高层次上,下载文件与检索数据相同。app需实现如下的代理方法:
URLSession:downloadTask:didFinishDownloadingToURL: 提供存储下载内容的临时文件的URL。
Important:在这个方法返回前,它必须打开文件来read或者把它移动到一个永久的位置。当这个方法返回时,如果文件还在原来的位置,它将会被删除。
URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:提供下载精度的状态信息。
- URLSession:downloadTask:didResumeAtOffset:expectedTotalBytes: 告诉app,尝试恢复以前失败的下载成功。
- URLSession:task:didCompleteWithError:下载失败
如果在后台session中下载,当app没有运行时这个下载还会继续。如果在一个standard或者ephemeral session中下载,当app重新启动时,下载必须重新开始。
在服务器传输数据的过程中,如果用户暂停下载,app使用cancelByProducingResumeData:方法来取消任务。过后,app把resume的数据传递给downloadTaskWithResumeData:或者 downloadTaskWithResumeData:completionHandler:方法,来创建一个新的下载任务来继续下载。
如果数据传输失败,代理方法的URLSession:task:didCompleteWithError: 将会调用。如果任务是可重新开始的,对象的 userInfo
字典会包含NSURLSessionDownloadTaskResumeData键的值。app应使用reachability APIs 来决定什么时候来再次尝试,然后调用downloadTaskWithResumeData: 或者 downloadTaskWithResumeData:completionHandler: 来创建新的下载任务来继续下载。
List 1-6 提供了一个下载中等大小文件的例子。List1-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);
}
上传Body内容
app可以为HTTP POST请求提供3种方式的body content:一个NSData
对象,一个文件,一个stream
- 使用
NSData
对象,如果在内存中有NSData对象,并且没有弃置它 - 使用file,如果你上传的内容已存在在磁盘上或者,把它写到磁盘上有释放内存的好处
- 使用stream,if you are receiving the data over a network or are converting existing NSURLConnection code that provides the request body as a stream.
不论你使用哪种方式,如果app提供了一个自定义的代理对象,代理对象应实现如下的方法:URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend: ,来获取上传进度信息。
另外,如果app使用stream来提供request body,它必须提供一个实现URLSession:task:needNewBodyStream:的自定义session代理,详细信息请参考Uploading Body Content Using a Stream.
使用NSData对象上传Body Content
为了使用NSdata对象,app可以调用uploadTaskWithRequest:fromData:或者uploadTaskWithRequest:fromData:completionHandler:方法来创建一个上传的任务,并通过fromData
参数来提供请求体数据。
session对象基于data对象的大小来计算Content-Length
的长度。
app也可能需要提供服务器需要提供的其它信息,如content type等。
使用File上传Body Content
app调用uploadTaskWithRequest:fromFile: 或者uploadTaskWithRequest:fromFile:completionHandler: 方法来创建一个上传任务,并提供一个任务用来读取body content的URL路径。
使用Stream上传Body Content
使用uploadTaskWithStreamedRequest: 方法来创建一个上传任务。