iOS 网络(1)——NSURLSession

最近公司针对刚入职的应届毕业生开展了一个的“新牛计划”,目的是让他们能够在一个月的时间内从零基础成长为 iOS 开发新手。

在这个过程中,我们需要承担讲师的角色。因此,我们对 iOS 开发的知识体系进行了划分,而我则负责讲解其中的 GCD 和网络相关部分。为此,我也算是学习了一下 iOS 开发所涉及到的一些网络知识,也学习了一些开源框架,包括:AFNetworking、YTKNetwork、CocoaAsyncSocket。这里,我首先对 NSURLSession 做一些相关总结。后续,将陆续贴出相关开源框架的学习心得。

NSURLSession 概述

WWDC 2013,苹果对基于 NSURLConnection 的 Foundation URL 加载系统进行了重构,推出了新一代基于 NSURLSession 的 Foundation URL 加载系统,并将其首先应用在了 iOS 7 和 Mac OS X 10.9 Mavericks 系统之中。

NSURLSession 架构

NSURLSession 这个名字,实际上是指代 Foundation 框架的 URL 加载系统中一些列相关的类和协议。上图所示为 NSURLSession 的系统架构图,主要由三个类构成:

  • NSURLSession
    • 负责请求/响应的关键对象,使用 NSURLSessionConfiguration 配置对象进行创建。
    • 在请求/响应的执行过程中调用 NSURLSessionTaskDelegate 所定义的各种代理方法。
  • NSURLSessionConfiguration
    • 用于对 NSURLSession 对象进行初始化,可以配置 可用网络Cookie安全性缓存策略自定义协议启动事件 等选项,以及用于移动设备优化的相关选项。
    • 几乎可以配置任何选项。
  • NSURLSessionTask
    • 一个抽象类,其子类可以创建不同类型的任务(Task),如:下载、上传、获取数据(如:JSON 或 XML)。
    • 在特定 URL Session 中执行。

结合上述系统结构图,我们可以将 NSURLSession 中的类分为以下 6 种(如下图所示):

  • URL 加载(URL Loading)
  • 配置管理(Configuration Management)
  • 缓存管理(Cache Policy)
  • Cookie 存储(Cookie Storage)
  • 认证和证书(Authentication and Credentials)
  • 协议支持(Protocol Support)

在一个请求被发送到服务器之前,系统会先查询共享的缓存信息,然后根据 缓存策略(Cache Policy) 以及 可用性(availability) 的不同,一个已经被缓存的响应可能会被立即返回。如果没有缓存的响应可用,则这个请求将根据我们指定的策略来缓存它的响应,以便将来的请求可以使用。

在一个请求被发送到服务器过程中,服务器可能会发出 鉴权查询(Authorization Challenge),这可以由共享的 Cookie 或 证书存储(Credential Storage) 来自动响应,或者由被委托对象来响应。此外,发送中的请求也可以被注册的 NSURLProtocol 对象所拦截,以便在必要时改变其加载行为。

下面我们依次来详细介绍 URL 加载系统中的 3 个主要类: NSURLSessionTaskNSURLSessionNSURLSessonConfiguration。在 NSURLSessionConfiguration 中,我们将对缓存策略、Cookie 存储、自定义协议等内容稍作介绍。

NSURLSessionTask

NSURLSessionTask 是一个抽象类,其包含如下 3 个实体子类。这 3 个子类封装了 3 个最基本的网络任务:获取数据(如:JSON 或 XML)、上传文件下载文件

  • NSURLSessionDataTask
  • NSURLSessionUploadTask
  • NSURLSessionDownloadTask

上图所示为这些类之间的继承关系。对于 NSURLSessionDataTask,服务器会有响应数据;而对于上传请求,服务器也会有响应数据,所以 NSURLSessionUploadTask 继承自 NSURLSessionDataTaskNSURLSessionDownloadTask 完成时,会带回已下载文件的一个临时的文件路径。

关于 NSURLSessionTask 的数据返回方式,主要有两种方式:

  • completionHandler 回调
  • NSURLSessionDelegate 代理

通过 completionHandler 回调将会创建一个隐式的代理(delegate),从而替代该 Task 原来的代理 —— Session。

对于需要 override 原有 Session Task 的代理的默认行为的情况,我们需要使用不带 completionHandler 版本。

需要注意的是,NSURLSessionTask 及其子类都有着各自的代理协议,它们之间也存在着如下图所示的继承关系。

  • NSURLSessionDelegate:定义了网络请求最基础的代理方法。作为所有代理的基类。
  • NSURLSessionTaskDelegate:定义了网络请求任务相关的代理方法。
  • NSURLSessionDownloadDelegate:定义了下载任务相关的代理方法,如:下载进度等
  • NSURLSessionDataDelegate:定义了普通数据任务和上传任务相关的代理方法。

下面简要介绍一下这三个子类。

NSURLSessionDataTask

NSURLSessionDataTask 主要用于 读取服务端的简单数据,如:JSON、XML 数据。

创建方法(基于 NSURLSession 对象)

1
2
3
4
5
// 使用 NSURLRequest 对象创建
- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request;

// 使用 NSURL 对象创建
- (NSURLSessionDataTask *)dataTaskWithURL:(NSURL *)url;

CompletionHandler

1
2
3
- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request completionHandler:(void (^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler;    

- (NSURLSessionDataTask *)dataTaskWithURL:(NSURL *)url completionHandler:(void (^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler;

NSURLSessionUploadTask

NSURLSessionUploadTask 主要用于 向服务器发送文件类型的数据

创建方法(基于 NSURLSession 对象)

1
2
3
4
5
6
7
// 使用 NSURLRequest 对象创建,上传时指定文件源
- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromFile:(NSURL *)fileURL;

// 使用 NSURLRequest 对象创建,上传时指定数据源
- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromData:(NSData *)bodyData;

- (NSURLSessionUploadTask *)uploadTaskWithStreamedRequest:(NSURLRequest *)request;

CompletionHandler

1
2
3
- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromFile:(NSURL *)fileURL completionHandler:(void (^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler;    

- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromData:(NSData *)bodyData completionHandler:(void (^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler;

NSURLSessionDownloadTask

NSURLSessionDownloadTask 主要用于 文件下载,它针对大文件的网络请求做了更多的处理,如:下载进度、断点续传等。

创建方法(基于 NSURLSession 对象)

1
2
3
4
5
6
7
8
// 使用 NSURLRequest 对象创建
- (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request;

// 使用 NSURL 对象创建
- (NSURLSessionDownloadTask *)downloadTaskWithURL:(NSURL *)url;

// 使用之前已经下载的数据来创建
- (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)resumeData;

CompletionHandler

1
2
3
4
5
- (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURL *location, NSURLResponse *response, NSError *error))completionHandler;    

- (NSURLSessionDownloadTask *)downloadTaskWithURL:(NSURL *)url completionHandler:(void (^)(NSURL *location, NSURLResponse *response, NSError *error))completionHandler;

- (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)resumeData completionHandler:(void (^)(NSURL *location, NSURLResponse *response, NSError *error))completionHandler;

NSURLSession

NSURLSession 是负责请求/响应的关键对象,使用 NSURLSessionConfiguration 配置对象进行创建。

NSURLSession 本身并不会进行请求,而是通过创建 Task 的形式来进行网络请求。同一个 NSURLSession 可以创建多个 Task,并且这些 Task 之间的 Cache 和 Cookie 是共享的。

NSURLSession 在管理请求/响应的过程中会调用相关的代理方法。这些代理方法主要分两类:

  • Session 的委托对象实现的代理方法(NSURLSessionDelegate 定义的方法)
    • 主要用于处理连接层问题,如:服务器信任、客户端证书认证、NTLM 和 Kerberos 协议等问题
  • Task 的委托对象实现的代理方法(NSURLSessionTaskDelegate 及其子协议定义的方法)
    • 主要用于处理以网络请求为基础的问题,如:Basic,Digest,代理身份验证(Proxy Authentication) 等。

NSURLSessionConfiguration

NSURLSessionConfiguration 对象用于对 NSURLSession 进行初始化。

NSURLSessionConfiguration 对以前 NSMutableURLRequest 所提供的网络请求层的设置选项进行了扩充,提供给开发者相当大的灵活性和控制权。从指定可用网络,到 cookie,安全性,缓存策略,再到使用自定义协议,启动事件的设置,以及用于移动设备优化的几个新属性,可以发现使用 NSURLSessionConfiguration 可以找到几乎任何想要进行配置的选项。

NSURLSession 在初始化时会把配置它的 NSURLSessionConfiguration 对象进行一次深拷贝,并保存到自己的 configuration 属性中,而且这个属性是只读的。也就是说,configuration 只在初始化时被读取一次,之后都是不会变化的。

初始化

NSURLSessionConfiguration 有三个类工厂方法:

  • + defaultSessionConfiguration
    • 返回一个标准的配置,具有共享 NSHTTPCookieStorage、共享 NSURLCache、共享 NSURLCredentialStorage
  • + ephemeralSessionConfiguration
    • 返回一个预设的配置,该配置中不会对缓存、Cookie和证书进行持久性存储。这对于实现类似秘密浏览这种功能来说是很理想的。
  • + backgroundSessionConfiguration:(NSString *)identifier
    • 创建一个后台 Session。后台 Session 不同于普通 Session,后台 Session 可以在应用程序挂起、退出或崩溃的情况下进行上传/下载任务。初始化时指定的标识符,可用于向任何可能在进程外恢复后台传输的 守护进程(daemon) 提供上下文。

属性配置

NSURLSessionConfiguration 拥有数十个配置属性。熟练掌握这些配置属性的用处,可以让应用程序充分地利用其网络环境。

常规配置

1
@property(copy) NSDictionary *HTTPAdditionalHeaders

HTTPAdditionalHeaders 为基于 configuration 的 Session 生成的所有 Task 中的 NSRULRequest 对象添加额外的请求头部字段。默认为空。

NSURLSession 默认为 NSURLRequest 对象添加了如下请求头部字段:

  • Authorization
  • Connection
  • Host
  • Proxy-Authenticate
  • Proxy-Authorization
  • WWW-Authenticate

如果在 HTTPAdditionalHeaders 自定义的头部字段与 NSURLRequest 对象重复了,则优先使用 NSURLRequest 对象中的请求头部字段。

利用 HTTPAddtionalHeaders 可以添加如下这些请求头部字段:

  • Accept
  • Accept-Language
  • User-Agent
  • ...
1
@property NSURLRequestNetworkServiceType networkServiceType

指定网络传输类型。可以让操作系统快速响应,提高传输质量,延长电池寿命等。大多数应用程序都不需要设置。

1
@property BOOL allowsCellularAccess

是否使用蜂窝网络。默认是 YES

1
@property NSTimeInterval timeoutIntervalForRequest

指定请求的超时间隔。默认为 60s。

1
@property NSTimeInterval timeoutIntervalForResource

指定资源的超时间隔。默认是7天。

1
@property(retain) NSHTTPCookieStorage *HTTPCookieStorage;

存储了 Session 所使用的 Cookie。默认情况下会使用 NSHTTPCookieStorage+ sharedHTTPCookieStorage 单例。

1
@property BOOL HTTPShouldSetCookies;

指定了请求是否应该使用 Session 存储的 Cookie,即 HTTPCookieStorage 属性的值。

1
@property NSHTTPCookieAcceptPolicy HTTPCookieAcceptPolicy;

决定了什么情况下 Session 应该接受从服务器发出的 Cookie。

安全策略

1
@property(retain) NSURLCredentialStorage *URLCredentialStorage;

存储了 Session 所使用的证书。默认情况下会使用 NSURLCredentialStorage+ sharedCredentialStorage 单例。

1
2
@property SSLProtocol TLSMaximumSupportedProtocol;
@property SSLProtocol TLSMinimumSupportedProtocol;

两者确定 Session 是否支持 SSL 协议。

缓存策略

1
@property(retain) NSURLCache *URLCache;

Session 使用的缓存。默认情况下会使用 NSURLCache+ sharedURLCache 单例。

1
@property NSURLRequestCachePolicy requestCachePolicy;

指定了一个请求的缓存响应应该在什么时候返回。

后台传输

1
@property(readonly, copy) NSString *identifier

仅当使用 backgroundSessionConfigurationWithIdentifier: 方法创建配置对象时,才会设置此属性的值。identifier 唯一标识 后台会话 对象。

如果应用程序在后台任务进行传输时终止,可以使用 identifier 在应用程序重新启动时,重新创建 configurationsession 对象与 之前传输进行关联。

1
@property BOOL sessionSendsLaunchEvents;

设置传输结束时是否应该在后台恢复或启动应用程序。

1
@property(getter=isDiscretionary) BOOL discretionary;

设置后台 Task 是否可以由系统进行调度,从而获得最佳性能。

1
@property BOOL shouldUseExtendedBackgroundIdleMode;

设置应用程序切换至后台时是否保持打开 TCP 连接。

自定义协议

1
@property(copy) NSArray<Class> *protocolClasses;

用来配置特定某个 Session 所使用的自定义协议(该协议是 NSURLProtocol 的子类)的数组。

多路径 TCP

1
@property NSURLSessionMultipathServiceType multipathServiceType;

指定通过 Wi-Fi 和 蜂窝网络传输数据的多路径 TCP 的连接策略。

HTTP 策略与代理

1
@property NSInteger HTTPMaximumConnectionsPerHost;

用于限制连接到特定主机的数量。

1
@property BOOL HTTPShouldUsePipelining;

用于开启 HTTP 流水线(HTTP pipelining),可以显着减少请求的加载时间,但是由于没有被服务器广泛支持,默认是 NO 的。

1
@property(copy) NSDictionary *connectionProxyDictionary;

指定了 Session 连接中的代理服务器

NSURLSession 使用

NSURLSession 的使用有如下几个步骤:

  1. 创建会话:基于 NSURLSessionConfiguration 对象创建 NSURLSession 对象
  2. 创建任务:基于 NSURLSession 对象创建 NSURLSessionTask 对象
  3. 执行任务:执行 NSURLSessionTask 对象

创建会话

会话的创建方式有三种:

1
2
3
4
5
6
7
8
9
10
// 1. 直接创建,使用默认的 NSURLSessionConfiguration 配置
NSURLSession *session = [NSURLSession sharedSession];

// 2. 配置后创建,先初始化一个 NSURLSessionConfiguration 对象
[NSURLSession sessionWithConfiguration:defaultSessionConfiguration];

// 3. 设置加代理获得
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]
delegate:self
delegateQueue:[[NSOperationQueue alloc] init]];

创建任务

任务的创建在上文介绍 NSURLSessionTask 时已经提到。这里不做赘述。

执行任务

1
2
// 执行任务
[task resume];

使用示例

GET 请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 1. 创建会话
NSURLSession *session = [NSURLSession sharedSession];

// 2. 创建任务
NSURL *url = [NSURL URLWithString:@"http://www.xxx.com/login?username=myName&pwd=myPsd"];

NSURLSessionDataTask *task = [session dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {

NSLog(@"%@", [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding]);
// 打印解析后的json数据
// NSLog(@"%@", [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil]);

}];

// 3. 执行任务
[task resume];

POST 请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 1. 创建会话
NSURLSession *session = [NSURLSession sharedSession];

// 2. 创建任务
NSURL *url = [NSURL URLWithString:@"http://www.xxx.com/login"];

// 创建请求对象里面包含请求体
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"POST";
request.HTTPBody = [@"username=myName&pwd=myPsd" dataUsingEncoding:NSUTF8StringEncoding];

NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {

NSLog(@"%@", [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding]);
// 打印解析后的json数据
// NSLog(@"%@", [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil]);

}];

// 3. 执行任务
[task resume];

结论

iOS 7 和 Mac OS X 10.9 Mavericks 中 URL 加载系统的变化,是对 NSURLConnection 进行深思熟虑后的一个自然而然的进化。尽管在这个体系结构中,某些决定对于可组合性和可扩展性而言是一种倒退,但是 NSURLSession 仍然是实现更高级别网络功能的一个强大的基础框架。

参考

  1. URL Loading System
  2. URL Loading System 概览
  3. iOS NSURLSession 详解
  4. URLSession Tutorial: Getting Started
  5. NSMutableURLRequest
  6. 从 NSURLConnection 到 NSURLSession
  7. From NSURLConnection to NSURLSession