URL Session的使用

URL Session相关类


URL加载

获取URL的Data数据(存储在内存)

这里有两种基本的方式获取URL的Data数据。

• 对于简单的请求,直接使用NSURLSession直接获取NSURL的数据或者直接使用NSData获取URL数据。

• 对于复杂的请求,使用NSURLSession和NSURLRequest或者NSMutableURLRequest。

获取响应数据。

• 提供completion handler block。

• 提供delegate。

下载文件

这里有两种基本的方式获取文件。

• 对于简单的请求,直接使用NSURLSession直接获取NSURL的数据或者直接使用NSData获取URL数据。

• 对于复杂的请求,使用NSURLSession和NSURLRequest或者NSMutableURLRequest。

URL Requests

URL Request用NSURLRequest和NSMutableURLRequest表示。NSURLRequest类封装了URL和使用的协议以及协议的规定。

响应的元数据

服务器的响应分内容元数据和内容数据。元数据包含MIME类型,内容长度,文本编码以及URL等信息。NSURLResponse的子类NSHTTPURLResponse提供头信息和状态码。

重定向和请求转移

例如,在HTTP协议,请求的内容已经转移到别的地方会告知APP从转移的地方获取内容。

验证和证书

一些服务器会请求用户提供证书或者用户名和密码等信息来获取内容。证书也可以用来确定是否信任服务器。

注意:存储到持久化仓库的证书会保存到钥匙扣并被其他APP共享。

NSURLCredential类封装了证书,包含验证信息(用户名和密码)和持久化行为。NSURLProtectionSpace类代表需要证书的空间,保护空间可以代表URL或者服务器又或者代理。

NSURLCredentialStorage对象管理会话的证书存储,它提供NSURLCredential和NSURLProtectionSpace的映射。只有在身份验证质询成功后,证书才会存储。

NSURLAuthenticationChallenge来封装了需要验证请求的信息:推荐证书,保护空间,错误,失败响应(用于决定验证是否需要),验证尝试的次数。NSURLAuthenticationChallenge实例指定sender对象来初始化验证,这个sender必须符合NSURLAuthenticationChallengeSender的协议。

缓存管理

使用NSURLCache来管理URL缓存。它可以设置缓存大小和磁盘位置,包含一组NSCachedURLResponse集合(包含缓存的响应)。NSCachedURLResponse封装了NSURLResponse和URL内容数据,它提供user info字典来缓存用户自定义数据。

并不是所有的协议都支持响应缓存。当前只有http和https。

Cookie仓库

由于http协议是无状态的,客服端经常使用cookie来持久化一些用户数据。URL加载系统提供接口创建和管理cookie,发送cookie作为http请求的一部分和获取cookie从服务器响应。

NSHTTPCookieStorage管理一组NSHTTPCookie集合。在OS X,Cookie仓库被所有APP共享。在IOS中,Cookie仓库只在APP内共享。

协议支持

URL加载系统支持http,https,file,ftp和data协议,你也可以注册自己的类和应用层网络协议。


使用NSURLSession

NSURLSession提供一组API实现下载、上传、验证、重定向、后台下载等功能。为了使用NSURLSession,你的APP创建一系列会话,每个会话可以创建一系列任务,每个任务对应一个网络请求。你可以使用completion handler block或者delegate获取响应。

注意:completion handler block是优先于delegate的完成方法,如果设置了block,那么delegate的完成方法不会执行。

NSURLSession提供状态和进度属性,而且分发这些信息到delegate。它支持取消、恢复、暂停任务和恢复那些暂停、取消、失败的任务。

理解URL Session概念

会话中的任务涉及3个方面:会话的类型(根据NSURLSessionConfiguration的类型),任务的类型,是否APP在前台。

会话类型

NSURLSession支持3种类型。

• 默认会话。和其他foundation框架的下载方法类似,使用基于磁盘的缓存和钥匙扣的证书。

• 短暂会话。不会保存任何数据到磁盘;所有缓存、证书等数据都保存在会话的内存中。一旦会话失效,它们就会被清除。

• 后台会话。和默认会话相似,除了使用新的进程处理数据的传输。后台会话有一些额外的限制。

任务类型

• 数据任务。传输和接收数据都是用NSData。数据任务一般用于短的、经常交互的请求。

• 下载任务。获取数据以文件形式。允许APP不在运行时,进行后台下载。

• 上传任务。发送数据以文件形式。允许APP不在运行时,进行后台上传。

后台传输注意事项

NSURLSession支持后台传输。你可以使用NSURLSessionConfiguration的后台会话设置来创建后台会话。

由于后台会话是用新的进程来传输数据的,而且重新启动你的APP来处理事件可能会消耗性能。下面是一些限制:

• 会话必须提供一个delegate获取事件。(对于上传和下载,delegate的行为是和进程内传输一样的)

• 只支持http和https。

• 总是服从重定向。

• 上传任务只支持文件上传。(在程序退出后,data和stream的上传会失败)

• 如果在后台初始化后台传输。NSURLSessionConfiguration对象的discretionary属性会设置为YES。(允许系统优化)

当后台传输完成或者请求证书时,如果你的APP不在运行,IOS会自动在后台重新启动APP并调用UIApplicationDelegate的application:handleEventsForBackgroundURLSession:completionHandler:方法。这个方法提供后台会话的标识以便你重新创建它,新的会话自动关联后台任务。你应该保存completionHandler以便在NSURLSession的delegate的URLSessionDidFinishEventsForBackgroundURLSession:方法中调用。completionHandler必须要在主线程调用,它让系统知道这个后台任务已经完成和让APP安全地进入暂停状态。

注意:你必须明确指定会话的标识,否则会话的行为可能不确定。

如果后台任务完成,NSURLSession会调用delegate的URLSession:downloadTask:didFinishDownloadingToURL:方法。

类似地,如果收到证书请求,NSURLSession会调用URLSession:task:didReceiveChallenge:completionHandler:方法或者URLSession:didReceiveChallenge:completionHandler:方法。

当遇到网络错误时,后台上传和后台下载任务会自动重试。

生命周期和delegate交互
NSCoping行为

NSURLSession和NSURLSessionTask对象是浅复制。

NSURLSessionConfiguration对象是深复制。

Delegate类的接口文件例子
@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上传和下载文件,内容数据和元数据分离。

• 设置每个主机最大的连接数量。

• 每个资源的超时时间。

• 最小和最大的TLS版本支持。

• 自定义协议字典。

• 设置Cookie的行为。

• 设置http管道传输行为。

大多数设置在NSURLSessionConfiguration中,你可以重复使用它。

创建NSURLSession。

• 设置NSURLSessionConfiguration。

• (可选)设置delegate来获取相关事件。

如果你不提供delegate,系统会默认提供delegate。你可以使用一些便利方法,例如sendAsynchronousRequest:queue:completionHandler:方法。

注意:后台传输必须指定delegate。

在你创建NSURLSession后,你就不能改变会话的configuration和delegate。

// 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:
 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_OSX
cachePath = [cachePath stringByStandardizingPath];
#endif
 
NSURLCache *cache = [[NSURLCache alloc] initWithMemoryCapacity:16384 diskCapacity:268435456 diskPath:cachePath];
defaultConfiguration.URLCache = cache;
defaultConfiguration.requestCachePolicy = NSURLRequestUseProtocolCachePolicy;
 
// Creating sessions
id <NSURLSessionDelegate> 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];
除了后台的configuration对象,其它的configuration对象都可以复用。(因为后台的configuration对象共用一个标识)

你可以安全地修改configuration对象,因为configuration对象是深复制的,所以新的对象不会改变原来的那个。

ephemeralConfiguration.allowsCellularAccess = NO;
NSURLSession *ephemeralSessionWiFiOnly = [NSURLSession sessionWithConfiguration:ephemeralConfiguration delegate:delegate delegateQueue:operationQueue];
使用系统提供的delegate获取资源

最直接使用NSURLSession获取资源就是使用系统提供的delegate。

• 设置NSURLSession的configuration。

• 提供completion handler处理响应。

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];
使用自定义delegate获取数据

如果设置自己的delegate,必须至少实现下面两个方法:

• URLSession:dataTask:didReceiveData:方法。

• URLSession:task:didCompleteWithError:方法。

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:告诉你下载失败。

如果你使用后台会话执行下载任务,即使APP不再运行下载也会继续。如果你使用的是默认或者短暂会话,重新启动APP时必须重新下载。

你可以在下载时使用下载任务的cancelByproducingResumeData:方法来暂停任务,之后,你可以使用这个方法返回的Resume Data和downloadTaskWithResumeData:或者downloadTaskWithResumeData:completionHandler:方法创建新的下载任务来继续下载。

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]) {
        // ...
    }
}
上传请求体内容

上传请求体内容分3种:NSData对象,文件,stream流。

• 如果你已经有上传内容的NSData对象而且不需要进一步处理,你应该使用NSData对象。

• 如果上传内容是已经在磁盘存在的文件,或者你需要后台传输,又或者你比较容易地进行写文件操作,你应该使用文件形式。

• 如果上传内容从网络上接收,你应该使用stream流形式。

如果你使用stream流形式,你必须实现URLSession:task:needNewBodyStream:代理方法。

上传数据使用NSData对象

上传NSData数据,你需要调用updateTaskWithRequest:fromData:或者updateTaskWithRequest:fromData:completionHandler:方法来创建上传任务。

会话对象会计算Content-Length头根据数据内容。

你的APP需要提供其他的请求头,例如Content-Type。

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:或者updateTaskWithRequest:fromFile:completionHandler:方法来创建上传任务。

会话对象会计算Content-Length头根据数据内容。

你的APP需要提供其他的请求头,例如Content-Type。

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];
上传数据使用stream流形式

上传stream流数据,你需要调用uploadTaskWithStreamedRequest:方法来创建上传任务。

你的APP需要提供其他的请求头,例如Content-Type,Content-Length。

因为会话的流不能够恢复,你必须实现URLSession:task: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对象或者Stream流形式创建下载任务。


处理验证和自定义TLS链验证

当收到服务器验证时,下面代理方法会被调用:

• 会话级别的Challenge-NSURLAuthenticationMethodNTLM,NSURLAuthenticationMethodNegotiate,NSURLAuthenticationMethodClientCertificate后者NSURLAuthenticationMethodServerTrust。 NSURLSession对象会调用URLSession:didReceiveChallenge:completionHandler:代理方法。如果你的APP没有提供NSURLSession的delegate对象,NSURLSession会调用URLSession:task:didReceiveChallenge:completionHandler:方法。

• 非会话级别的Challenge,NSURLSession会调用任务的URLSession:task:didReceiveChallenge:completionHandler:方法。会话的URLSession:didReceiveChallenge:completionHandler:代理方法不会由于非会话级别的Challenge而被调用,你必须实现任务级别的代理方法。


处理IOS后台活动

如果你使用NSURLSession,当下载任务完成后,你的APP会自动重新启动。APP的application:handleEventsForBackgroundURLSession:completionHandler:代理方法会被调用。你应该获取适当的Session和存储completionHandler,以便在NSURLSession的URLSessionDidFinishEventsForBackgroundURLSession:代理方法中调用。

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

编码和解码URL数据

为了实现percent-encode URL字符串,调用stringByAddingPercentEncodingWithAllowedCharacters:方法和提供适当的字符串集。

• User:URLUserAllowedCharacterSet

• Password:URLPasswordAllowedCharacterSet

• Host:URLHostAllowedCharacterSet

• Path:URLPathAllowedCharacterSet

• Fragment:URLFragmentAllowedCharacterSet 

• Query:URLQueryAllowedCharacterSet

注意:不要用stringByAddingPercentEncodingWithAllowedCharacters:方法来编码整个URL字符串,因为URL字符串每一部分都有不同的规则。

NSString *originalString = @"color-#708090";
NSCharacterSet *allowedCharacters = [NSCharacterSet URLFragmentAllowedCharacterSet];
NSString *percentEncodedString = [originalString stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacters];
NSLog(@"%@", percentEncodedString"); // prints "color-%23708090"

如果你想要编码URL字符串组件,你需要使用NSURLComponents来划分组件。

NSURL *URL = [NSURL URLWithString:@"https://example.com/#color-%23708090"];
NSURLComponents *components = [NSURLComponents componentsWithURL:URL resolvingAgainstBaseURL:NO];
NSString *fragment = components.fragment;
NSLog(@"%@", fragment); // prints "color-#708090"


处理重定向和其它请求变化

重定向是服务器响应请求指示客户端应该使用新的URL获取内容。你需要实现URLSession:task:willPerformHTTPR额direction:newRequest:completionHandler:代理方法来处理该事件。

在这个代理方法你可以:

• 允许重定向。直接返回提供的请求。

• 创建新的请求返回。

• 拒绝重定向。返回nil。

而且你也可以取消重定向和连接,直接在代理方法中取消任务(调用cancel方法)。

- (void)URLSession:(NSURLSession *)session
        task:(NSURLSessionTask *)task
        willPerformHTTPRedirection:(NSHTTPURLResponse *)redirectResponse
        newRequest:(NSURLRequest *)request
        completionHandler:(void (^)(NSURLRequest *))completionHandler
{
    NSURLRequest *newRequest = request;
    if (redirectResponse) {
        newRequest = nil;
    }
 
    completionHandler(newRequest);
}

如果没有实现这个代理方法,所有的重定向默认允许。


验证Challenge和TLS链

决定怎样响应验证Challenge

这里有3种方式响应:

• 提供验证证书。

• 尝试不用证书验证。

• 取消验证。

NSURLAuthenticationChallenge实例包含一些信息:导致验证Challenge触发的原因,尝试的次数,之前尝试的证书。NSURLProtectionSpace请求证书和Challenge发送者。

如果验证Challenge之前验证失败,(例如,用户修改了密码)你可以从Challenge的proposedCredential属性获取之前尝试的证书。在这个代理方法,你可以使用这些证书来获取历史记录。

调用Challenge对象的previousFailureCount属性获取之前总共失败的次数。你可以利用这个属性来确定之前尝试的证书是否失败过后者确定在尝试多少次。

响应验证Challenge

下面是3种方法响应URLSession:didReceiveChallenge:completionHandler或者URLSession:task:didReceiveChallenge:completionHandler:代理方法。

提供证书

为了提供验证,你应该创建一个NSURLCredential对像。首先你先要确定验证方法是什么?你可以通过college的protectionSpace的authenticationMethod属性获取验证方法。

• HTTP基本验证(NSURLAuthenticationMethodHTTPBasic)请求用户名和密码。提示用户输入必要信息并调用credentialWithUser:password:persistence:方法创建NSURLCredential。

• HTTP摘要验证(NSURLAuthenticationMethodHTTPDigest),像基本验证需要用户名和密码。(摘要自动生成)使用credentialWithUser:password:persistence:方法创建。

• 客户端证书验证(NSURLAuthenticationMethodClientCertificate)请求系统标识和服务器需要验证的证书。使用credentialWithIdentity:certificates:persistence:方法创建。

• 服务器信任验证(NSURLAuthenticationMethodServerTrust)请求Challenge的protectionSpace的serverTrust。使用credentialForTrust:方法创建。

在你创建完NSURLCredential对象,传递NSURLCredential对象到completion handler block。

不使用证书

如果代理对象不提供证书给验证Challenge,这会尝试不使用证书完成验证。传递下面的值给completion handler block:

• NSURLSessionAuthChallengePerformDefaultHandling 系统默认的处理验证请求的方式(如果不实现代理方法使用该方式)。

• NSURLSessionAuthChallengeRejectProtectionSpace 拒绝challenge。根据服务器允许的验证方式,URL加载系统会调用该代理方法多次。

取消连接

你可以通过传递NSURLSessionAuthChallengeCancelAuthenticationChallenge给completion handler block来取消连接。

验证例子

通过提供用户名和密码来创建NSURLCredential来响应请求。如果失败,取消验证challenge并通知用户。

- (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
  completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * __nullable credential))completionHandler
{
    if ([challenge previousFailureCount] == 0) {
        NSURLCredential *newCredential = [NSURLCredential credentialWithUser:[self preferencesName]
                                                                    password:[self preferencesPassword]
                                                                 persistence:NSURLCredentialPersistenceNone];
        completionHandler(NSURLSessionAuthChallengeUseCredential, newCredential);
    } else {
        // Inform the user that the user name and password are incorrect
        completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
    }
}

如果这个验证challenge没有被会话和任务代理处理而且没有可用的证书或者验证失败,continueWithoutCredentialForAuthenticationChallenge:方法会被底层实现并调用。

执行自定义的TLS链验证

在NSURL类族AIP中,TLS链验证需要你的APP实现验证代理方法,并不是提供证书验证服务器(或者你的APP),你的APP需要在TLS握手中验证服务器证书,之后告诉URL加载系统是否接收或者拒绝证书。

如果你需要使用非标准的方式来执行链验证(例如,接收指定的自签名证书来测试),你的APP必须实现 URLSession:didReceiveChallenge:completionHandler:或者 URLSession:task:didReceiveChallenge:completionHandler:代理方法,如果你同时实现,会话级别的方法有责任处理验证。

在代理方法中,你应该检查challenge的保护空间的验证方法是否是NSURLAuthenticationMethodServerTrust,如果是,从保护空间获取serverTrust信息。


理解缓存获取

URL加载系统提供复杂的磁盘和内存的响应缓存。缓存允许应用减少网络连接的依赖和提高性能。

对请求使用缓存

NSURLRequest对象可以指定本地缓存的怎样使用,你可以设置缓存政策(NSURLRequestCachePolicy):NSURLRequestUseProtocolCachePolicy,NSURLRequestReloadIgnoringCacheData,NSURLRequestReturnCacheDataElseLoad,NSURLRequestReturnCacheDataDontLoad。

NSURLRequest对象的默认缓存政策是NSURLRequestUseProtocolCachePolicy。这个政策是协议指定的而且是效果最佳的。

如果设置缓存政策为NSURLRequestReloadIgnoringCacheData,URL加载系统会直接从指定的URL加载数据,完全避免使用缓存。

如果设置缓存政策为NSURLRequestReturnCacheDataElseLoad,URL加载系统会先寻找缓存不管缓存的生存期或者过期日期,如果没有命中缓存则加载数据。

如果这只缓存政策为NSURLRequestReturnCacheDataDontLoad,URL加载系统只会从缓存获取响应,如果没有命中缓存则直接返回nil。这个政策类似于离线模式,不会连接网络获取数据。

注意:目前只有HTTP和HTTPS协议提供响应缓存,其它协议不提供响应缓存,直接加载URL指定数据。

HTTP协议的缓存使用语义

最复杂的缓存使用情形是HTTP协议使用NSURLRequestUseProtocolCachePolicy缓存政策。

如果这个请求的NSCachedURLResponse不存在,URL加载系统从指定的URL加载数据。

如果这个请求的NSCachedURLResponse存在,URL加载系统需要检查这个响应缓存的内容是否被指定为重新验证。

如果这个内容必须重新验证,URL加载系统会向指定的URL数据源发送HEAD请求来确定内容是否改变。如果没有改变,URL加载系统会返回这个响应缓存。如果已经改变,URL加载系统会加载数据。

如果响应缓存的内容没有指定为重新验证,URL加载系统会检查缓存响应的生存期或者过期时间。如果响应缓存时最近的,URL加载系统会返回这个响应缓存。如果响应缓存是久的,URL加载系统会向指定的URL数据源发送HEAD请求来确定内容是否改变,如果是,URL加载系统会加载数据,否则,会返回响应缓存。

详细请查看http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13

程序控制缓存

通常,请求的数据会根据缓存政策来进行缓存。

如果你的APP需要精确的程序控制缓存(如果这个协议支持缓存),你可以实现代理方法来决定每一个请求的指定响应是否被缓存。

对于NSURLSession的数据和上传任务,实现URLSession:dataTask:willCacheResponse:completionHandler: 方法。这个代理方法只会在数据和上传任务调用。下载任务的缓存政策需要专门提供。

对于NSURLSession,你的代理方法需要调用completion handler block来告诉会话缓存什么。代理通常会提供下面一些值:

• 提供允许缓存的响应对象。

• 新建一个响应缓存对象来缓存修改的响应 - 例如,响应的存储政策为内存缓存而不是磁盘缓存。

• 提供nil来阻止缓存。

你的代理方法可以向响应缓存对象的userInfo字典插入对象,会造成这些对象会和响应缓存对象一起被缓存。

注意:你的代理方法必须调用completion handler block,否则会造成内存泄漏。

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
 willCacheResponse:(NSCachedURLResponse *)proposedResponse
 completionHandler:(void (^)(NSCachedURLResponse * __nullable cachedResponse))completionHandler {
    NSCachedURLResponse *newCachedResponse = proposedResponse;
    NSDictionary *newUserInfo;
    newUserInfo = [NSDictionary dictionaryWithObject:[NSDate date]
                                              forKey:@"Cached Date"];
    if ([proposedResponse.response.URL.scheme isEqualToString:@"https"]) {
#if ALLOW_IN_MEMORY_CACHING
        newCachedResponse = [[NSCachedURLResponse alloc]
                             initWithResponse:proposedResponse.response
                             data:proposedResponse.data
                             userInfo:newUserInfo
                             storagePolicy:NSURLCacheStorageAllowedInMemoryOnly];
#else // !ALLOW_IN_MEMORY_CACHING
        newCachedResponse = nil;
#endif // ALLOW_IN_MEMORY_CACHING
    } else {
        newCachedResponse = [[NSCachedURLResponse alloc]
                             initWithResponse:[proposedResponse response]
                             data:[proposedResponse data]
                             userInfo:newUserInfo
                             storagePolicy:[proposedResponse storagePolicy]];
    }
 
    completionHandler(newCachedResponse);
}

Cookies和自定义协议

如果你的APP需要程序上管理cookies,例如添加和删除cookies或者决定哪个cookies应该被获取。

如果你的APP需要一个基本的URL协议,但是NSURL不支持。你可以注册你自己的自定义协议类来提供这个需求。

Cookie存储

由于HTTP协议是无状态的,客户端经常使用cookie来提供持久化存储URL请求数据。URL加载系统提供接口来创建和管理cookies,发送cookie作为HTTP请求的一部分,解析响应获取cookie。

NSHTTPCookie封装cookie,它提供大多数通用cookie属性的访问方法。这个类提供HTTP的cookie头和NSHTTPCookie实例互相转换的方法。URL加载系统自动发送对应NSURLRequest的存储cookie,除非指定不发生cookie。同样的,cookie会根据访问政策返回NSURLResponse。

NSHTTPCookieStorage提供接口去管理一组NSHTTPCookie,这些NSHTTPCookie对象可以被所有APP访问。

注意:IOS不支持Cookie在APP间共享。

NSHTTPCookieStorage允许APP指定cookie访问访问政策。cookie访问政策控制包含可访问、不可访问、只能被相同域(主文档URL)访问。

注意:在APP中改变cookie访问政策也会作用于其他APP。

当另外的APP改变cookie存储或者cookie访问政策,NSHTTPCookieStorage会发生NSHTTPCookieManagerCookiesChangedNotification和NSHTTPCookieStorageAcceptPolicyChangedNotification通知。

协议支持

URL加载系统允许客户端APP去扩展传输数据协议。URL加载系统本地支持http、https、file、ftp、data协议。

你可以提供继承NSURLProtocol来实现自定义协议并使用NSURLProtocol的registerClass:方法来注册新的类。当NSURLSession对象初始化NSURLRequest连接,URL加载系统会按注册的反顺序询问注册的类。这个类在canInitWithRequest:返回YES,它将会用来处理请求。

如果你的自定义协议需要为请求和响应添加额外的属性,你可以创建NSURLRequest,NSMutableURLRequest,NSURLResponse种类来添加额外属性的访问方法。NSURLProtocol提供方法来使用这些方法设置和获取额外的属性值。

在连接开始和完成时,URL加载系统负责创建和释放NSURLProtocol实例。你的APP不要直接创建NSURLProtocol实例。

当NSURLProtocol子类被URL加载系统初始化,它提供一个符合NSURLProtocolClient协议的客户端。NSURLProtocol子类会从NSURLProtocolClient协议客户端发送消息去通知URL加载系统,这些包括创建响应,接收数据,重定向新URL,请求验证,加载完成。如果自定义协议支持验证,它必须实现NSURLAuthenticationChallengeSender协议。


URL会话的声明周期

你可以使用NSURLSession的API的两个方式:系统提供delegate或者你自己提供delegate。通常,如果你要完成下面的任务,你必须使用自己的delegate:

• 当你的APP不在运行时,使用后台会话来下载或者上传内容。

• 执行自定义验证。

• 执行自定义的SSL证书验证。

• 确定传输是否应该下载到磁盘或者根据MIME类型来展示。

• 上传数据来自数据流。

• 程序上限制缓存。

• 程序上限制HTTP重定向。

如果你的APP不需要做任何这样的事情,你的APP可以使用系统提供的delegate。

系统提供delegate的URL会话声明周期

如果你使用NSURLSession而且不提供delegate对象,系统提供的delegate对象会处理大部分细节。下面是你必须提供的基本的方法调用顺序和提供完成handler。

1. 创建一个会话设置(session configuration)。对于后台会话,这个会话设置必须设置一个唯一标识。保存这个标识,在应用崩溃、终止、暂停使用它。

2.创建一个会话,指定会话设置对象或者nil。

3.使用会话创建任务对象,这个对象代表资源请求。

每一个任务开始状态为暂停。在你调用任务的resume方法后,它开始下载指定的资源。

NSURLSessionTask任务对象的子类-NSURLSessionDataTask,NSURLSessionUploadTask,NSURLSessionDownloadTask。

虽然你的APP可能(应该)在会话中添加超过一个任务,简单起见,接下来的步骤以一个单独的任务来说明声明周期。

注意:如果你使用NSURLSession并且不提供delegate对象,你必须使用completionHandler来获取数据。

4.对于下载任务,在传输过程中,如果用户告诉你暂停任务,调用cancelByProducingResumeData:方法来取消任务。之后,把恢复数据(Resume Data)传递给downloadTaskWithResumeData:或者downloadTaskWithResumeData:completionHandler:方法来创建一个新的下载任务来继续下载。

5.当任务完成后,NSURLSession对象会调用任务的completionHandler。

注意:NSURLSession不会提供error参数来报告服务器错误。你的APP只会接收到客户端这边的错误信息,例如,不能解析主机名或者不能连接主机。

服务器那边的错误会通过NSHTTPURLResponse的HTTP状态码来表示。

6.当你的APP不再需要会话,通过invalidateAndCancel(取消当前任务)或者finishTasksAndInvalidate(允许当前任务在失效会话前完成)方法来失效会话。

自定义delegate的URL会话生命周期

你可能经常提供delegate对象来使用NSURLSession。但是,如果你使用NSURLSession进行后台下载和上传,或者如果你需要处理验证或者使用非默认的缓存行为,你必须提供一个会话代理对象,一个或者多个任务代理对象,或者符合这些代理协议的代理对象。

• 当你使用下载任务,NSURLSession对象提供delegate来提供下载完成的数据的本地URL。

这些代理对象必须实现NSURLSessionDownloadDelegate协议。

• 代理能够处理一些验证评估。

• 代理提供上传流数据的body流。

• 代理能够决定是否跟随HTTP重定向。

• NSURLSession对象提供delegate来提供传输状态。数据任务(Data Task)的代理接收初始化回调,这个回调能转换请求为下载任务和接收一系列的下载数据进程调用。

• NSURLSession告诉代理下载完成。

如果你提供自定义代理对象给URL会话(请求后台任务),完成的URL会话生命周期会更复杂。下面是一系列方法调用,这些方法你必须实现。

1.创建会话设置(session configuration)。对于后台会话,这个会话设置必须设置一个唯一标识。保存这个标识,在应用崩溃、终止、暂停使用它。

2.创建会话(session),指定设置对象和代理对象。

3.使用会话创建任务对象,这个对象代表资源请求。

每一个任务开始状态为暂停。在你调用任务的resume方法后,它开始下载指定的资源。

NSURLSessionTask任务对象的子类-NSURLSessionDataTask,NSURLSessionUploadTask,NSURLSessionDownloadTask。

虽然你的APP可能(应该)在会话中添加超过一个任务,简单起见,接下来的步骤以一个单独的任务来说明声明周期。

注意:如果你使用NSURLSession并且不提供delegate对象,你必须使用completionHandler来获取数据。

4.如果远程服务器返回状态码来指示验证请求而且如果验证请求是连接阶段的咨询(challenge)(例如SSL客户端证书),NSURLSession调用验证咨询(authentication challenge)代理方法。

• 对于会话级别的咨询-NSURLAuthenticationMethodNTLM,NSURLAuthenticationMethodNegotiate,NSURLAuthenticationMethodClientCertificate,NSURLAuthenticationMethodServerTrust-NSURLSession对象会调用会话代理的URLSession:didReceiveChallenge:completionHandler:方法。如果你的APP没有实现这个方法,NSURLSession对象会调用任务代理的URLSession:task:didReceiveChallenge:completionHandler:方法来处理咨询。

• 对于不是会话级别的咨询,NSURLSession对象会调用任务代理的URLSession:task:didReceiveChallenge:completionHandler:方法来处理咨询。你必须提供任务代理的验证方法或者提供在任务级别handler调用会话级别的handler。会话代理的URLSession:didReceiveChallenge:completionHandler:方法不会被不是会话级别的咨询触发。

如果上传任务验证失败,如果任务的数据是流数据,NSURLSession对象会调用URLSession:task:needNewBodyStream:代理方法。这个代理方法必须提供一个新的NSInputStream对象来获取body数据。

5.当收到HTTP重定向响应,NSURLSession对象会调用代理的URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:方法。这个代理方法提供completion handler;你需要提供NSURLRequest对象(使用提供的newRequest)给completion handler,或者是创建一个新的NSURLRequest对象,又或者是nil(不进行重定向)。

• 如果跟随重定向,回到第4步(验证咨询处理)。

• 如果代理不实现这个方法,默认跟随重定向。

6.对于恢复下载任务((re-)download task),使用downloadTaskWithResumeData:或者downloadTaskWithResumeData:completionHandler:方法来重新创建恢复下载任务。NSURLSession对象会调用代理的URLSession:downloadTask:didResumeAtOffset:expectedTotalBytes:方法来表示下载任务恢复成功。

7.对于数据任务(Data task),NSURLSession对象调用代理的URLSession:dataTask:didReceiveResponse:completionHandler:方法。决定是否转换数据任务为下载任务,之后调用completion callback来继续获取数据或者下载数据。

如果你的APP选择转换数据任务为下载任务,NSURLSession会调用代理的URLSession:dataTask:didBecomeDownloadTask:方法来获取新的下载任务对象。在这个方法调用成功后,代理不会收到任何数据任务的回调并开始接收下载任务的回调。

8.如果使用uploadTaskWithStreamedRequest:方法创建任务,NSURLSession对象会调用URLSession:task:needNewBodyStream:方法来提供body数据。

9.在进行上传任务时,代理会周期性调用URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:回调来报告上传进程。

10.在进行下载任务时,代理会周期性调用URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:方法来报告下载进程。对于数据任务,会话会调用代理的URLSession:dataTask:didReceiveData:方法来获取每次实际接收的数据。

对于下载任务,如果用户暂停下载,调用cancelByProducingResumeData:方法来取消任务。

之后,如果用户恢复下载,提供恢复数据并调用downloadTaskWithResumeData:或者downloadTaskWithResumeData:completionHandler:方法来创建新的下载任务来继续下载,之后回到步骤3(创建和恢复任务对象)。

11.对于数据任务,NSURLSession对象会调用代理的URLSession:dataTask:willCacheResponse:completionHandler:方法。你的APP需要决定是否允许缓存。如果你没有实现这个方法,默认行为会根据会话设置对象的缓存政策。

12.如果下载任务成功完成,NSURLSession对象会调用URLSession:downloadTask:didFinishDownloadingToURL:方法,这个方法会提供下载文件的临时存储URL。你的APP可以读取这个文件的内容或者在这个方法返回前,移动这个文件到沙盒的某个地方。

13.如果任务完成,NSURLSession对象会调用代理的URLSession:task:didCompleteWithError:方法。

如果任务失败,大多数APP应该重新尝试请求直到用户取消下载或者服务器返回错误指示请求不可能成功。你的APP不应该马上重新请求,而是使用reachability API来决定是否服务器是可访问的和在收到服务器可访问通知时才创建新的请求。

如果下载任务能够恢复,NSError对象的userInfo字典会包含NSURLSessionDownloadTaskResumeData键值对。你的APP应该是这个键的值并调用downloadTaskWithResumeData:或者downloadTaskWithResumeData:completionHandler:来创建新的下载任务来继续下载。

如果任务不能够恢复,你的APP应该创建新的下载任务并重新启动新的事务。

如果传输失败由于各种原因除了服务器错误,回到步骤3(创建并恢复任务对象)。

14.如果响应是multipart编码,会话可能会再次调用代理的didReceiveResponse方法,之后是0次或者更多次地调用didReceiveData方法。如果发生,回到步骤7(处理didReceiveResponse方法)。

15.当你的APP不再需要会话,通过invalidateAndCancel(取消当前任务)或者finishTasksAndInvalidate(允许当前任务在失效会话前完成)方法来失效会话。

在失效会话后,当所有当前任务被取消或者完成,会话会调用代理的URLSession:didBecomeInvalidWithError:方法。当代理方法返回,会话会处理代理的强引用。

注意:会话会保持代理的强引用直到你明确失效会话。如果你没有失效会话,你的APP会内存泄露。

如果你的APP取消正在下载任务,NSURLSession对象会调用代理的URLSession:task:didCompleteWithError: 方法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章