URL Session Programming Guide - Using NSURLSession

官方文档

Using NSURLSession

(使用NSURLSession)

NSURLSession及其相关的类提供了通过HTTP协议下载内容的API。这些API提供了丰富的代理方法支持认证,并且让你的应用程序在没有运行(在iOS中即是挂起时)时具有后台执行下载任务的能力。

为了使用NSURLSession的API,你的应用程序创建了一系列的会话,每一个会话都协调了一组数据传输相关的任务。比如,如果你在开发一个web浏览器,你的应用程序可能会每个标签或者窗口创建一个会话。对于每个会话,你的应用程序可能会添加一系列的任务到会话中,每个任务表示了一个特定URL的请求(以及任何返回的HTTP重定向)。

类似于大多数网络请求API,NSURLSession的API是高度异步的。如果你使用默认的NSURLSession,系统会提供代理,你必须提供一个处理完成的Block,用来在传输成功完成或者发生了错误时给你的应用程序返回数据。另外,如果你提供了你自己自定义的代理对象,任务对象在接收来自服务器的数据后调用这些代理的方法。

注意:完成回调主要用来当作一种可替代的自定义代理。如果你使用接收完成回调的方法创建了一个任务,那么用来传输响应和数据的代理方法就不会被调用。

NSURLSession的API提供了状态和进度属性,除了将这些信息传递给代理。它也支持取消,重新开始(恢复),挂起任务,也提供了恢复挂起,取消,或者下载失败的任务的能力。

Understanding URL Session Concepts

(理解URL Session的概念)

在一个会话中,任务的行为依赖于三件事情:会话的类型(取决于创建会话时使用的配置对象),任务的类型,以及任务创建时应用程序是否在前台。

Types of Sessions

(会话类型)

NSURLSession的API支持三种类型的会话,由创建会话时配置对象的类型决定:

  • Default类型的会话的行为类似于其它用来下载URL内容的Foundation方法。使用持久化的基于磁盘的缓存和存储在用户钥匙串理的凭证。
  • Ephemeral类型的会话不会存储任何数据到磁盘;所有的缓存,凭证等等都保存在内存中,与会话绑定。因此,当你的应用程序无效的时候,他们也被自动抹去了。
  • Background类型的会话类似于Default类型,除了一个分开的进程处理所有的数据传输。Background会话有些额外的限制,在 Background Transfer Considerations 中描述了。

Types of Tasks

(任务类型)

对于会话,NSURLSession类支持三种类型的任务:data tasks,download tasks,and upload tasks。

  • Data tasks使用NSData对象发送和接收数据。Data tasks主要针对你应用程序中那些与服务器短的、经常交互的请求。Data tasks能够一次返回一小段数据,或者通过完成处理句炳一次返回。
  • Download tasks在文件形式中检索数据,支持在应用程序没有运行时进行后台下载任务。
  • Upload tasks以文件形式发送数据,支持在应用程序没有运行时进行后台上传任务。

Background Transfer Considerations

(后台传输的注意事项)

NSURLSession类支持应用程序被挂起时后台传输。只有使用后台会话的配置对象创建的NSURLSession对象才支持后台传输(使用 backgroundSessionConfiguration: 创建后台会话的配置对象)。

因为实际传输在分开的进程中处理,以及重启你应用程序的进程是相对昂贵的,所以一些功能无法实现,导致了如下所示的限制:

  • 会话必须给每一个传输提供一个代理。(对于上传和下载,代理的行为类似于处理传输。)
  • 只有HTTP和HTTPS协议支持(自定义协议不支持)。
  • 始终遵循重定向。
  • 只支持基于文件的上传任务(基于数据对象和流的上传任务在程序退出时就会失败)。
  • 如果后台传输是应用程序在后台时初始化的,那么配置对象的 discretionary 属性应该设置为true

注意:在iOS8和OS X 10.10之前,Data tasks不支持后台会话。

iOS和OS X重新启动应用程序的行为有一些不一样。

在iOS中,当一个后台传输完成或者需要凭证,如果你的应用程序没有在运行,iOS在后台自动重启你的应用程序然后在应用程序的UIApplicationDelegate对象上调用application:handleEventsForBackgroundURLSession:completionHandler:方法。这个方法提供了导致你应用程序重启的会话的identifier。你的应用程序应该存储完成处理句柄,使用这个相同的identifier创建一个后台配置对象,然后使用这个后台配置对象创建一个会话。新的会话会自动与后台活动的关联。之后,当会话完成了最后一个后台下载任务,它会给会话的代理发送一个URLSessionDidFinishEventsForBackgroundURLSession:消息。你的会话代理应该调用存储的完成句柄。

在iOS和OS X中,当用户重启了你的应用程序,你的应用程序应该立即使用相同的identifier为应用程序最后运行的任务创建后台配置的对象,然后为每一个配置对象创建一个会话。这些新的会话会自动与后台活动的关联。

注意:每个identifier只能创建一个会话(创建配置对象的时候指定)。多个会话共享同一个identifier的行为是不确定。

当应用程序挂起的时候,有任务完成,代理的URLSession:downloadTask:didFinishDownloadingToURL:方法会被调用,以及与新的下载文件相关的任务和URL。

类似的,如果任务需要凭证,NSURLSession对象调用代理的URLSession:task:didReceiveChallenge:completionHandler:方法或者URLSession:didReceiveChallenge:completionHandler:方法。

后台会话中的上传和下载的任务在网络错误后,URL加载系统会自动重试。所以没有必要使用reachability的API来侦测网络确定何时重试失败的任务。

NSURLSession后台传输的例子,参见Simple Background Transfer

Life Cycle and Delegate Interaction

(声明周期和代理交互)

取决于你使用NSURLSession类做什么,了解会话完整的生命周期可能很有帮助,包括和代理的交互,以及代理调用的顺序,当服务器返回重定向的时候会发生什么,以及当你的应用程序恢复一个下载失败的任务时发生了什么,等等。

关于会话生命周期的完整描述,参见 Life Cycle of a URL Session

NSCopying Behavior

(NSCopying行为)

会话和任务对象按照如下适配NSCopying协议:

  • 当你的应用程序复制会话或者任务对象时,你会得到同一个对象。
  • 当你的应用程序复制一个配置对象,你会得到一个新的拷贝的对象,便于你可以独立的修改。

Sample Delegate Class Interface

(简单的代理类接口)

接下来的代码片段基于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

Creating and Configuring a Session

(创建配置会话)

NSURLSession的API提供了广泛的配置选项:

  • 支持给单个的会话指定私有的缓存,Cookie,凭证,和协议。
  • 依赖于特定请求(task)或者一组请求(session)的认证。
  • 通过URL上传和下载文件,鼓励数据(文件内容)和源数据分离。
  • 配置每个主机的最大连接数。
  • 如果一整个资源在一定的时间下不能下载就会触发每个资源超时。
  • 最低和最高的TLS版本支持。
  • 自定义的代理字典。
  • 控制Cookie策略。
  • 控制HTTP管道行为。

因为大部分设置都包含在一个分开的配置对象里,你可以重用相同的设置。当你初始化一个会话对象时,按照如下:

  • 一个配置对象,管理会话和任务的行为。
  • 一个可选的代理对象,用来处理接收的数据以及处理指定给会话和任务的其它事件,比如服务器认证,确定资源请求是否应该被转换成下载等。

如果你没有提供代理对象,NSURLSession对象会使用系统提供的代理。通过这种方式,你可以很容易的使用NSURLSession来替换已经存在的使用 sendAsynchronousRequest:queue:completionHandler: 方法的代码。

注意:如果的应用程序需要执行后台传输,你必须提供一个自定义的代理。

在你初始化一个会话对象后,你不能改变配置或者代理,除了重新创建一个新的会话。

Listing 1-2展示了如何创建一个normal, ephemeral, 和 background的会话的例子。

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]];

除了后台配置,你可以重用其他的配置对象创建额外的会话。(你不能重用后台会话配置,因为两个后台会话对象共享相同的identifier的行为不确定的。)

任何时候修改配置对象都是安全的。当你创建会话的时候,会话会对配置对象执行深拷贝,所以修改只会影响新创建的会话,不会影响已经存在的。比如,你可能创建了第二个会话,只在Wi-Fi连接下才获取内容,如Listing 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的最直接的方法就是使用NSURLSession上的方法替换 sendAsynchronousRequest:queue:completionHandler: 方法。使用这种方法,你只需要完成两部分代码:

  • 创建一个配置对象,然后使用配置对象创建一个会话。
  • 当数据完全接受后的处理句柄。

使用系统提供的代理,你可以只使用一行代码就能发起获取特定URL的请求。Listing 1-4演示了这个简单的例子。

注意:系统提供的代理只在自定义网络行为的时候有限制。如果你的应用程序需要除了基本请求之外的特别行为,比如自定义的认证或者后台下载,这种技术就不太适合。对于你必须实现完整代理的情形,参见Life Cycle of a 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

(使用自定义代理获取数据)

如果你使用自定义代理获取数据,你的代理至少要实现如下方法:

如果你的应用程序需要使用URLSession:dataTask:didReceiveData:方法返回的数据,那么你的代码负责存储这些数据。

比如,一个Web浏览器可能在接收到数据的时候就要呈现数据。要做到这一点,你可能会使用一个字典映射一个任务对象和存储结果的NSMutableData,然后使用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];

Downloading Files

(下载文件)

在更高层级上来说,下载文件类似于接收数据。你的应用程序应该实现如下代理方法:

如果你在后台会话上执行下载,当你应用程序没有下载的时候仍然会继续执行。如果你在默认或者短暂(ephemeral)的会话上运行,下载必须在你应用程序启动后重新开始。

在与服务器传输数据期间,如果用户告诉应用程序暂停下载,你的应用程序可以调用 cancelByProducingResumeData: 方法来取消任务。之后,你的应用程序可以传递已经下载好的数据给 downloadTaskWithResumeData: 或者 downloadTaskWithResumeData:completionHandler: 方法来创建一个继续之前下载的新的下载任务。

如果传输失败,你的代理的调用 URLSession:task:didCompleteWithError: 方法,并传递一个NSError对象。如果任务是可恢复的,对象的userInfo字典的 NSURLSessionDownloadTaskResumeData 键会包含一个已经下载了数据的值。你的应用程序可以传递这个已经下载好的数据给 downloadTaskWithResumeData: 或者 downloadTaskWithResumeData:completionHandler: 方法创建一个新的下载任务,重新开始下载。

Listing 1-6提供了一个下载中等文件大小的例子。Listing 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

(上传正文内容)

你的应用程序可以有三种方式给HTTP POST请求提供请求的正文内容(Body Content):作为一个NSData对象,文件或者流。通常来说,你的应用程序应该:

  • 使用NSData对象,如果你的内存中已经有了数据并且没有理由去处理它。
  • 如果你上传的内容已经在磁盘文件中,如果你使用后台传输,或者将数据写入磁盘减少内存对你的应用程序有好处,那么你就使用文件。
  • 如果你通过网络接收数据,使用流。或者转换提供给NSURLConnection请求正文的流的代码。

不管你选择哪种方式,如果你的应用程序提供了自定义的会话代理,代理需要实现 URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend: 代理方法来获得上传进度的信息。

另外,如果你的应用程序使用流提供请求正文,必须提供一个自定义会话代理,实现 URLSession:task:needNewBodyStream: 方法,更多详细内容,参见 Uploading Body Content Using a Stream

Uploading Body Content Using an NSData Object

(使用NSData对象上传正文内容)

使用NSData对象上传正文内容,你的应用程序调用 uploadTaskWithRequest:fromData: 或者 uploadTaskWithRequest:fromData:completionHandler: 方法创建一个上传任务,然后通过fromData参数提供请求的正文数据。

会话对象会基于数据对象的大小来计算Content-Length首部字段。

你的应用程序必须提供服务器要求的额外的首部字段信息,比如内容类型(content type),作为URL请求对象的一部分。

Uploading Body Content Using a File

(使用文件上传正文内容)

使用文件上传正文内容,你的应用程序可以调用 uploadTaskWithRequest:fromFile: uploadTaskWithRequest:fromFile:completionHandler: 方法创建上传任务,然后提供一个文件的URL,上传任务会从这个URL读取正文内容。

会话对象根据数据对象计算首部字段Content-Length的值。如果你的应用程序没有为首部字段提供Content-Type的值,会话对象也会提供一个。

你的应用程序应该提供服务器要求的首部字段信息,作为URL请求对象的一部分。

Uploading Body Content Using a Stream

(使用流上传正文内容)

使用流上传正文内容,调用 uploadTaskWithStreamedRequest: 方法创建一个上传任务。你应用程序提供一个请求对象,这个对象与流相关联,上传任务会从流里面读取正文内容。

你的应用程序必须提供服务器要求的首部字段的信息,内容类型和内容长度,作为URL请求对象的一部分。

Uploading a File Using a Download Task

(使用下载任务(Download Task)上传文件)

为下载任务(download task)上传正文内容,你的应用程序必须提供NSData对象或者流作为URL请求对象的一部分,然后用这个请求创建下载请求。

如果你使用流提供数据,你的应用程序必须提供一个 URLSession:task:needNewBodyStream: 代理方法在认证失败的事件中来提供新的内容流。这个方法的详细信息,参见 Uploading Body Content Using a Stream

下载任务(download task)的行为类似于数据任务(data task),除了返回数据的方式不一样。

Handling Authentication and Custom TLS Chain Validation

(处理认证以及自定义TLS链验证)

如果远程服务器返回了一个状态码指示需要认证,如果认证要求一个连接级别的挑战(比如SSL客户端证书),NSURLSession调用认证的代理方法。

注意:Kerberos认证的处理是透明的。

当一个基于流的上传任务的认证失败时,任务不能回退且安全的重用流。相反,NSURLSession对象会调用代理的 URLSession:task:needNewBodyStream: 方法去获得一个新的NSInputStream对象,给新的请求提供数据。(如果上传任务是基于文件或者NSData对象,会话对象就不会调用这个方法。)

关于NSURLSession代理中的认证的方法的更多信息,参见 Authentication Challenges and TLS Chain Validation

Handling iOS Background Activity

(处理iOS后台活动)

如果你在iOS中使用NSURLSession,当下载完成时应用程序会自动重启。你应用程序的app delegate的 application:handleEventsForBackgroundURLSession:completionHandler: 方法会负责重新创建合适的会话,存储完成处理器,当会话调用会话代理的 URLSessionDidFinishEventsForBackgroundURLSession: 方法时会调用那个处理器。

Listing 1-8 和 Listing 1-9分开展示会话及app delegate的方法。

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];
}
坚持原创技术分享,您的支持将鼓励我继续创作!