从 NSURLConnection 到 NSURLSession

iOS 7 和 Mac OS X 10.9 Mavericks 中一个显著的变化就是对 Foundation URL 加载系统的彻底重构。

现在已经有人在深入苹果的网络层基础架构的地方做研究了,所以我想是时候来分享一些对于我对于这些新的 API 的看法和心得了,新的 API 将如何影响我们编写程序,以及它们对于 API 设计理念的影响。

NSURLConnection

作为 Core Foundation / CFNetwork 框架的 API 之上的一个抽象,在 2003 年,随着第一版的 Safari 的发布就发布了。NSURLConnection这个名字,实际上是指代的 Foundation 框架的 URL 加载系统中一系列有关联的组件:NSURLRequest、NSURLResponse、NSURLProtocol、 NSURLCache、 NSHTTPCookieStorage、NSURLCredentialStorage 以及同名类 NSURLConnection。

NSURLRequest被传递给 NSURLConnection。被委托对象(遵守以前的非正式协议 <NSURLConnectionDelegate>和 <NSURLConnectionDataDelegate>)异步地返回一个 NSURLResponse以及包含服务器返回信息的 NSData。
在一个请求被发送到服务器之前,系统会先查询共享的缓存信息,然后根据策略(policy)以及可用性(availability)的不同,一个已经被缓存的响应可能会被立即返回。如果没有缓存的响应可用,则这个请求将根据我们指定的策略来缓存它的响应以便将来的请求可以使用。

在把请求发送给服务器的过程中,服务器可能会发出鉴权查询(authentication challenge),这可以由共享的 cookie 或机密存储(credential storage)来自动响应,或者由被委托对象来响应。发送中的请求也可以被注册的 NSURLProtocol对象所拦截,以便在必要的时候无缝地改变其加载行为。
不管怎样,NSURLConnection作为网络基础架构,已经服务了成千上万的 iOS 和 Mac OS 程序,并且做的还算相当不错。但是这些年,一些用例——尤其是在 iPhone 和 iPad 上面——已经对 NSURLConnection
的几个核心概念提出了挑战,让苹果有理由对它进行重构。
在 2013 的 WWDC 上,苹果推出了 NSURLConnection的继任者:NSURLSession。
和 NSURLConnection一样,NSURLSession指的也不仅是同名类 NSURLSession,还包括一系列相互关联的类。NSURLSession包括了与之前相同的组件,NSURLRequest与 NSURLCache,但是把 NSURLConnection替换成了 NSURLSession、NSURLSessionConfiguration以及 NSURLSessionTask的 3 个子类:NSURLSessionDataTask,NSURLSessionUploadTask,NSURLSessionDownloadTask。
与 NSURLConnection相比,NSURLsession最直接的改进就是可以配置每个 session 的缓存,协议,cookie,以及证书策略(credential policy),甚至跨程序共享这些信息。这将允许程序和网络基础框架之间相互独立,不会发生干扰。每个 NSURLSession 对象都由一个 NSURLSessionConfiguration对象来进行初始化,后者指定了刚才提到的那些策略以及一些用来增强移动设备上性能的新选项。
NSURLSession中另一大块就是 session task。它负责处理数据的加载以及文件和数据在客户端与服务端之间的上传和下载。NSURLSessionTask与 NSURLConnection
最大的相似之处在于它也负责数据的加载,最大的不同之处在于所有的 task 共享其创造者 NSURLSession
这一公共委托者(common delegate)
我们先来深入探讨 task,过后再来讨论 NSURLSessionConfiguration。

NSURLSessionTask

NSURLsessionTask是一个抽象类,其下有 3 个实体子类可以直接使用:NSURLSessionDataTask、NSURLSessionUploadTask、NSURLSessionDownloadTask。这 3 个子类封装了现代程序三个最基本的网络任务:获取数据,比如 JSON 或者 XML,上传文件和下载文件。

NSURLSessionTask class diagram

当一个 NSURLSessionDataTask完成时,它会带有相关联的数据,而一个 NSURLSessionDownloadTask任务结束时,它会带回已下载文件的一个临时的文件路径。因为一般来说,服务端对于一个上传任务的响应也会有相关数据返回,所以 NSURLSessionUploadTask继承自 NSURLSessionDataTask。
所有的 task 都是可以取消,暂停或者恢复的。当一个 download task 取消时,可以通过选项来创建一个恢复数据(resume data),然后可以传递给下一次新创建的 download task,以便继续之前的下载。
不同于直接使用 alloc-init初始化方法,task 是由一个 NSURLSession
创建的。每个 task 的构造方法都对应有或者没有 completionHandler
这个 block 的两个版本,例如:有这样两个构造方法 –dataTaskWithRequest:和 –dataTaskWithRequest:completionHandler:
。这与 NSURLConnection的 -sendAsynchronousRequest:queue:completionHandler:
方法类似,通过指定 completionHandler这个 block 将创建一个隐式的 delegate,来替代该 task 原来的 delegate——session。对于需要 override 原有 session task 的 delegate 的默认行为的情况,我们需要使用这种不带 completionHandler的版本。

NSURLSessionTask 的工厂方法

在 iOS 5 中,NSURLConnection添加了 sendAsynchronousRequest:queue:completionHandler:
这一方法,对于一次性使用的 request, 大大地简化代码,同时它也是 sendSynchronousRequest:returningResponse:error:
这个方法的异步替代品:

NSURL *URL = [NSURL URLWithString:@"http://example.com"];
 NSURLRequest *request = [NSURLRequest requestWithURL:URL]; 
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] 
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) { // ... }];

NSURLSession
在 task 的构造方法上延续了这一模式。不同的是,这里不会立即运行 task,而是将该 task 对象先返回,允许我们进一步的配置,然后可以使用 resume方法来让它开始运行。
Data task 可以通过 NSURL或 NSURLRequest创建(使用前者相当于是使用一个对于该 URL 进行标准 GET请求的 NSURLRequest
,这是一种快捷方法):

NSURL *URL = [NSURL URLWithString:@"http://example.com"]; 
NSURLRequest *request = [NSURLRequest requestWithURL:URL]; 
NSURLSession *session = [NSURLSession sharedSession]; 
NSURLSessionDataTask *task = [session dataTaskWithRequest:request 
completionHandler: ^(NSData *data, NSURLResponse *response, NSError *error) { // ... }];
 [task resume];

Upload task 的创建需要使用一个 request,另外加上一个要上传的 NSData
对象或者是一个本地文件的路径对应的 NSURL:

NSURL *URL = [NSURL URLWithString:@"http://example.com/upload"];
 NSURLRequest *request = [NSURLRequest requestWithURL:URL]; 
NSData *data = ...; NSURLSession *session = [NSURLSession sharedSession]; 
NSURLSessionUploadTask *uploadTask = [session uploadTaskWithRequest:request fromData:data 
completionHandler: ^(NSData *data, NSURLResponse *response, NSError *error) { // ... }];
 [uploadTask resume];

Download task 也需要一个 request,不同之处在于 completionHandler
这个 block。Data task 和 upload task 会在任务完成时一次性返回,但是 Download task 是将数据一点点地写入本地的临时文件。所以在 completionHandler这个 block 里,我们需要把文件从一个临时地址移动到一个永久的地址保存起来:

NSURL *URL = [NSURL URLWithString:@"http://example.com/file.zip"]; 
NSURLRequest *request = [NSURLRequest requestWithURL:URL]; 
NSURLSession *session = [NSURLSession sharedSession]; 
NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithRequest:request 
completionHandler: ^(NSURL *location, NSURLResponse *response, NSError *error) { NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
NSURL *documentsDirectoryURL = [NSURL fileURLWithPath:documentsPath];
 NSURL *newFileLocation = [documentsDirectoryURL URLByAppendingPathComponent:[[response URL] lastPathComponent]];
 [[NSFileManager defaultManager] copyItemAtURL:location toURL:newFileLocation error:nil]; 
}]; 
[downloadTask resume];

编者注 原文中这块代码以及上文的表述中存有一些问题,详见这个 issue,本文已进行更正,如果您有不同意见,欢迎在 Github 上给我们反馈。

NSURLSession 与 NSURLConnection 的 delegate 方法

总体而言,NSURLSession的 delegate 方法是 NSURLConnection
的演化的十年中对于 ad-hoc 模式的一个显著改善。您可以查看这个映射表来进行一个完整的概览。
以下是一些具体的观察:
NSURLSession既拥有 seesion 的 delegate 方法,又拥有 task 的 delegate 方法用来处理鉴权查询。session 的 delegate 方法处理连接层的问题,诸如服务器信任,客户端证书的评估,NTLMKerberos 协议这类问题,而 task 的 delegate 则处理以网络请求为基础的问题,如 Basic,Digest,以及代理身份验证(Proxy authentication)等。
在 NSURLConnection中有两个 delegate 方法可以表明一个网络请求已经结束:NSURLConnectionDataDelegate中的 -connectionDidFinishLoading:和 NSURLConnectionDelegate
中的 -connection:didFailWithError:,而在 NSURLSession
中改为一个 delegate 方法:NSURLSessionTaskDelegate
的 -URLSession:task:didCompleteWithError:

NSURLSession中表示传输多少字节的参数类型现在改为 int64_t,以前在 NSURLConnection中相应的参数的类型是 long long。
由于增加了 completionHandler:这个 block 作为参数,NSURLSession实际上给 Foundation 框架引入了一种全新的模式。这种模式允许 delegate 方法可以安全地在主线程与运行,而不会阻塞主线程;Delgate 只需要简单地调用 dispatch_async就可以切换到后台进行相关的操作,然后在操作完成时调用 completionHandler 即可。同时,它还可以有效地拥有多个返回值,而不需要我们使用笨拙的参数指针。以 NSURLSessionTaskDelegate 的 -URLSession:task:didReceiveChallenge:completionHandler:方法来举例,completionHandler接受两个参数:NSURLSessionAuthChallengeDisposition和 NSURLCredential
,前者为应对鉴权查询的策略,后者为需要使用的证书(仅当前者——应对鉴权查询的策略为使用证书,即 NSURLSessionAuthChallengeUseCredential时有效,否则该参数为 NULL)
想要查看更多关于 session task 的信息,可以查看 WWDC Session 705: "What’s New in Foundation Networking"

NSURLSessionConfiguration

NSURLSessionConfiguration对象用于对 NSURLSession对象进行初始化。NSURLSessionConfiguration对以前 NSMutableURLRequest
所提供的网络请求层的设置选项进行了扩充,提供给我们相当大的灵活性和控制权。从指定可用网络,到 cookie,安全性,缓存策略,再到使用自定义协议,启动事件的设置,以及用于移动设备优化的几个新属性,你会发现使用 NSURLSessionConfiguration可以找到几乎任何你想要进行配置的选项。
NSURLSession在初始化时会把配置它的 NSURLSessionConfiguration
对象进行一次 copy,并保存到自己的 configuration属性中,而且这个属性是只读的。因此之后再修改最初配置 session 的那个 configuration 对象对于 session 是没有影响的。也就是说,configuration 只在初始化时被读取一次,之后都是不会变化的。
NSURLSessionConfiguration 的工厂方法NSURLSessionConfiguration
有三个类工厂方法,这很好地说明了 NSURLSession
设计时所考虑的不同的使用场景。
+defaultSessionConfiguration
返回一个标准的 configuration,这个配置实际上与 NSURLConnection的网络堆栈(networking stack)是一样的,具有相同的共享 NSHTTPCookieStorage,共享 NSURLCache
和共享 NSURLCredentialStorage。
+ephemeralSessionConfiguration
返回一个预设配置,这个配置中不会对缓存,Cookie 和证书进行持久性的存储。这对于实现像秘密浏览这种功能来说是很理想的。

+backgroundSessionConfiguration:(NSString *)identifier

的独特之处在于,它会创建一个后台 session。后台 session 不同于常规的,普通的 session,它甚至可以在应用程序挂起,退出或者崩溃的情况下运行上传和下载任务。初始化时指定的标识符,被用于向任何可能在进程外恢复后台传输的守护进程(daemon)提供上下文。
想要查看更多关于后台 session 的信息,可以查看 WWDC Session 204: "What's New with Multitasking"

配置属性

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

基本配置

HTTPAdditionalHeaders
指定了一组默认的可以设置出站请求(outbound request)的数据头。这对于跨 session 共享信息,如内容类型,语言,用户代理和身份认证,是很有用的。

NSString *userPasswordString = [NSString stringWithFormat:@"%@:%@", user, password];
NSData * userPasswordData = [userPasswordString dataUsingEncoding:NSUTF8StringEncoding];
NSString *base64EncodedCredential = [userPasswordData base64EncodedStringWithOptions:0];
NSString *authString = [NSString stringWithFormat:@"Basic %@", base64EncodedCredential];
NSString *userAgentString = @"AppName/com.example.app (iPhone 5s; iOS 7.0.2; Scale/2.0)";
configuration.HTTPAdditionalHeaders = @{@"Accept": @"application/json", @"Accept-Language": @"en", @"Authorization": authString, @"User-Agent": userAgentString};

networkServiceType
对标准的网络流量,网络电话,语音,视频,以及由一个后台进程使用的流量进行了区分。大多数应用程序都不需要设置这个。
allowsCellularAccess和 discretionary
被用于节省通过蜂窝网络连接的带宽。对于后台传输的情况,推荐大家使用 discretionary这个属性,而不是 allowsCellularAccess,因为前者会把 WiFi 和电源的可用性考虑在内。
timeoutIntervalForRequest和 timeoutIntervalForResource
分别指定了对于请求和资源的超时间隔。许多开发人员试图使用 timeoutInterval
去限制发送请求的总时间,但其实它真正的含义是:分组(packet)之间的时间。实际上我们应该使用 timeoutIntervalForResource
来规定整体超时的总时间,但应该只将其用于后台传输,而不是用户实际上可能想要去等待的任何东西。
HTTPMaximumConnectionsPerHost
是 Foundation 框架中 URL 加载系统的一个新的配置选项。它曾经被 NSURLConnection用于管理私有的连接池。现在有了 NSURLSession,开发者可以在需要时限制连接到特定主机的数量。
HTTPShouldUsePipelining
这个属性在 NSMutableURLRequest
下也有,它可以被用于开启 HTTP 管线化(HTTP pipelining,这可以显着降低请求的加载时间,但是由于没有被服务器广泛支持,默认是禁用的。
sessionSendsLaunchEvents
是另一个新的属性,该属性指定该 session 是否应该从后台启动。
connectionProxyDictionary
指定了 session 连接中的代理服务器。同样地,大多数面向消费者的应用程序都不需要代理,所以基本上不需要配置这个属性。
关于连接代理的更多信息可以在 CFProxySupportReference 找到。

Cookie 策略

HTTPCookieStorage存储了 session 所使用的 cookie。
默认情况下会使用 NSHTTPCookieShorage
的 +sharedHTTPCookieStorage这个单例对象,这与 NSURLConnection 是相同的。
HTTPCookieAcceptPolicy
决定了什么情况下 session 应该接受从服务器发出的 cookie。
HTTPShouldSetCookies
指定了请求是否应该使用 session 存储的 cookie,即 HTTPCookieSorage 属性的值。

安全策略

URLCredentialStorage
存储了 session 所使用的证书。默认情况下会使用 NSURLCredentialStorage 的 +sharedCredentialStorage
这个单例对象,这与 NSURLConnection是相同的。
TLSMaximumSupportedProtocol和 TLSMinimumSupportedProtocol
确定 session 是否支持 SSL 协议

缓存策略

URLCache
是 session 使用的缓存。默认情况下会使用 NSURLCache的 +sharedURLCache这个单例对象,这与 NSURLConnection
是相同的。
requestCachePolicy
指定了一个请求的缓存响应应该在什么时候返回。这相当于 NSURLRequest 的 -cachePolicy 方法。

自定义协议

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

结论

iOS 7 和 Mac OS X 10.9 Mavericks 中 URL 加载系统的变化,是对 NSURLConnection 进行深思熟虑后的一个自然而然的进化。总体而言,苹果的 Foundation 框架团队干了一件令人钦佩的的工作,他们研究并预测了移动开发者现有的和新兴的用例,创造了能够满足日常任务而且非常好用的 API 。
尽管在这个体系结构中,某些决定对于可组合性和可扩展性而言是一种倒退,但是 NSURLSession 仍然是实现更高级别网络功能的一个强大的基础框架。
原文 From NSURLConnection to NSURLSession

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容