深入了解NSURLSession

Github : Jerry4me, Demo : JRBgSessionDemo

前言

本文主要是结合官方文档, 挖掘NSURLSession的类层次结构及其联系, 总结出关于NSURLSession的一些关键点及其用法.

关于NSURLSession为什么能取代NSURLConnection, 其优势是什么, 及其NSURLSession API的概述, 见
  • iOS7 NSURLSession.
关于ATS, HTTP/2, 以及iOS9 NSURLSession新特性 : sharedCookies, streamTask和taskMetrics, 见
  • iOS9 ATS HTTP/2 NSURLSession

以上两篇文章都是我看wwdc视频然后总结出来的文章, 大家感兴趣的可以先了解了解. 如果不想知道那么多, 只想知道怎么用NSURLSession, 那就直接看本文的正文.

*了解URL Loading System
深入了解NSURLSession_第1张图片

目录

  • 介绍NSURLSession相关类
  • 身份验证和自定义TLS
  • [App Transport Security](#App Transport Security)
  • [NSURLSession 工作流程](#NSURLSession 工作流程)
  • [后台传输及其用法](#Background Transport)
  • [NSURLSession API](#NSURLSession API)
  • [其他一些注意点](#Something else Important)

NSURLSession

NSURLSession

  • 支持data, ftp, http(s)协议, 同时支持代理服务器和socks网关.
  • 支持http/1.1, http/2, spdy协议, 但同时需要服务器支持ALPN和NPN.
ALPN与NPN
  • ALPN(Application Layer Protocol Negotiation,应用层协议协商)
  • NPN(Next Protocol Negotiation,下一代协议协商)

NPN是服务端发送它支持的HTTP协议列表, 供客户端选择; 而ALPN则相反, 由客户端发送它支持的HTTP协议列表, 供服务端选择. 如果缺少NPN/ALPN其中一个, 则无法使用HTTP/2通信. 具体请见为什么我们应该尽快支持 ALPN.

NSURLSession相关类为 :

  • NSURLSession
  • NSURLSessionConfiguration
  • NSURLSessionDelegate
  • NSURLSessionTask
  • NSURLSessionTaskMetrics
  • NSURLSessionTaskTransactionMetrics

他们相互的关系如下 :

深入了解NSURLSession_第2张图片
NSURLSession

session分为 :

  • 全局共享单例session : NSURLSession sharedSession, 有一定的局限性
  • 自定义session : 自定义配置文件, 设置代理, 大部分时间我们都是用这个
  • 后台session : 也是自定义session的一种, 只是他专门用于做后台上传/下载任务

session为哪一种类型完全由其内部的Configuration而定.

NSURLSessionConfiguration

配置分为 :

  • defaultSessionConfiguration : 系统默认
  • ephemeralSessionConfiguration : 仅内存缓存, 不做磁盘缓存的配置
  • backgroundSessionConfiguration : 这里需要指定一个identifier, identifier用来后台重连session对象. (做后台上传/下载就是这个config)

另外, 我们还可以给Configuration对象再自定义一些属性, 例如每端口的最大并发HTTP请求数目, 以及是否允许蜂窝网络, 请求缓存策略, 请求超时, cookies/证书存储策略等等

NSURLSessionDelegate
深入了解NSURLSession_第3张图片

session管理的一组tasks共享一个代理, 不想实现代理方法时, 代理传nil即可.
代理协议分为 :

  • NSURLSessionDelegate : session-level的代理方法
  • NSURLSessionTaskDelegate : task-level面向all的代理方法
  • NSURLSessionDataDelegate : task-level面向data和upload的代理方法
  • NSURLSession​Download​Delegate : task-level的面向download的代理方法
  • NSURLSessionStreamDelegate : task-level的面向stream的代理方法
NSURLSessionTask
深入了解NSURLSession_第4张图片

session task类型分为 :

  • NSURLSessionTask : Task的抽象基类
  • NSURLSessionDataTask : 以NSData的形式接收一个URLRequest的内容
  • NSURLSessionUploadTask : 上传NSData或者本地磁盘中的文件, 完成后以NSData的形式接收一个URLRequest的响应
  • NSURLSessionDownloadTask : 下载完成后返回临时文件在本地磁盘的URL路径
  • NSURLSessionStreamTask : 用于建立一个TCP/IP连接
NSURLSessionTaskMetrics 和 NSURLSessionTaskTransactionMetrics

对发送请求/DNS查询/TLS握手/请求响应等各种环节时间上的统计. 更易于我们检测, 分析我们App的请求缓慢到底是发生在哪个环节, 并对此进行优化提升我们APP的性能.

NSURLSessionTaskMetrics对象与NSURLSessionTask对象一一对应. 每个NSURLSessionTaskMetrics对象内有3个属性 :

  • taskInterval : task从开始到结束总共用的时间
  • redirectCount : task重定向的次数
  • transactionMetrics : 一个task从发出请求到收到数据过程中派生出的每个子请求, 它是一个装着许多NSURLSessionTaskTransactionMetrics对象的数组. 每个对象都代表下图的一个子过程
深入了解NSURLSession_第5张图片

API很简单, 就一个方法 : - (void)URLSession: task: didFinishCollectingMetrics:, 当收集完成的时候就会调用该方法.

身份验证和自定义TLS

  1. 当一个服务器请求身份验证或TLS握手期间需要提供证书的话, URLSession会调用他的代理方法URLSession:​did​Receive​Challenge:​completion​Handler:​去处理.

  2. 如果你没有实现该代理方法, URLSession就会这么做 :

  • 使用身份认证信息作为请求URL的一部分(如果可用的话)
  • 在用户的keychain中查找网络密码和证书(in macOS), 在app的keychain中查找(in iOS)
  1. 如果证书还是不可用或服务器拒绝该证书, 就会继续缺少身份认证的连接.
  • 对于HTTP(S)连接, 请求失败并返回一个状态码, 可能会提供一些替代的内容, 例如一个私人网站的公共网页.
  • 对于其他URL类型(如FTP等), 则连接请求失败, 直接返回错误信息

App Transport Security

从iOS9开始支持ATS, 且默认ATS只支持发送HTTPS请求, 不允许发送不安全的HTTP请求. 如果用户需要发送HTTP请求需要在info.plist中配置一些东西.

详情在文章开头的iOS9 ATS HTTP/2 NSURLSession中说得很详细, 想了解的可以进去阅读.

NSURLSession 工作流程

那么如何使用NSURLSession像从前用NSURLConnection那样发送一个请求呢?

// 设置配置
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
/** 设置其他配置属性 **/

// 代理队列
NSOperationQueue *queue = [NSOperationQueue mainQueue];

// 创建session
NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:queue];

// 利用session创建n个task
NSURLSessionDownloadTask *task = [session downloadTaskWithURL:[NSURL URLWithString:@"https://www.baidu.com"]];
// 开始
[task resume];

然后就可以在代理方法中处理各种事情了. 简单吧? 下面分task说明代理方法的调用情况..

身份验证或TLS握手

这是所有task都必须经历的一个过程. 当一个服务器请求身份验证或TLS握手期间需要提供证书的话, URLSession会调用他的代理方法URLSession:​did​Receive​Challenge:​completion​Handler:​去处理., 另外, 如果连接途中收到服务器返回需要身份认证的response, 也会调用该代理方法.

重定位response

这也是所有task都有可能经历的一个过程, 如果response是HTTP重定位, session会调用代理的URLSession:​task:​will​Perform​HTTPRedirection:​new​Request:​completion​Handler:方法. 这里需要调用completionHandler告诉session是否允许重定位, 或者重定位到另一个URL, 或者传nil表示重定位的响应body有效并返回. 如果代理没有实现该方法, 则允许重定位直到达到最大重定位次数.

DataTask

  1. 对于一个data task来说, session会调用代理的URLSession:​data​Task:​did​Receive​Response:​completion​Handler:​方法, 决定是否将一个data dask转换成download task, 然后调用completion回调继续接收data或下载data.
  • 如果你的app选择转换成download task, session会调用代理的URLSession:​data​Task:​did​Become​Download​Task:​方法并把新的download task对象以方法参数的形式传给你. 之后代理不会再收到data task的回调而是转为收到download task的回调
  1. 在服务器传输数据给客户端期间, 代理会周期性地收到URLSession:​data​Task:​did​Receive​Data:​回调

  2. session会调用URLSession:​data​Task:​will​Cache​Response:​completion​Handler:​询问你的app是否允许缓存. 如果代理不实现这个方法的话, 默认使用session绑定的Configuration的缓存策略.

DownloadTask

  1. 对于一个通过download​Task​With​Resume​Data:​创建的下载任务, session会调用代理的URLSession:​download​Task:​did​Resume​At​Offset:​expected​Total​Bytes:​方法.

  2. 在服务器传输数据给客户端期间, 调用URLSession:​download​Task:​did​Write​Data:​total​Bytes​Written:​total​Bytes​Expected​To​Write:给用户传数据

  • 当用户暂停下载时, 调用cancel​By​Producing​Resume​Data:​给用户传已下好的数据.
  • 如果用户想要恢复下载, 把刚刚的resumeData以参数的形式传给download​Task​With​Resume​Data:​方法创建新的task继续下载.
  1. 如果download task成功完成了, 调用URLSession:​download​Task:​did​Finish​Downloading​To​URL:把临时文件的URL路径给你. 此时你应该在该代理方法返回以前读取他的数据或者把文件持久化.

UploadTask

上传数据去服务器期间, 代理会周期性收到URLSession:​task:​did​Send​Body​Data:​total​Bytes​Sent:​total​Bytes​Expected​To​Send:回调并获得上传进度的报告.

StreamTask

如果任务的数据是由一个stream发出的, session就会调用代理的URLSession:​task:​need​New​Body​Stream:​方法去获取一个NSInputStream对象并提供一个新请求的body data.

task completion

任何task完成的时候, 都会调用URLSession:​task:​did​Complete​With​Error:​方法, error有可能为nil(请求成功), 不为nil(请求失败)

  • 请求失败, 但是该任务是可恢复下载的, 那么error对象的userInfo字典里有一个NSURLSession​Download​Task​Resume​Data对应的value, 你应该把这个值传给download​Task​With​Resume​Data:​方法重新恢复下载

  • 请求失败, 但是任务无法恢复下载, 那么应该重新创建一个下载任务并从头开始下载.

  • 因为其他原因(如服务器错误等等), 创建并恢复请求.

Note
NSURLSession不会收到服务器传来的错误, 代理只会收到客户端出现的错误, 例如无法解析主机名或无法连接上主机等等. 客户端错误定义在URL Loading System Error Codes. 服务端错误通过HTTP状态法进行传输, 详情请看NSHTTPURLResponse和NSURLResponse类.

销毁session

如果你不再需要一个session了, 一定要调用它的invalidateAndCancelfinishTasksAndInvalidate方法. (前者是取消所有未完成的任务然后使session失效, 后者是等待正在执行的任务完成之后再使session失效). 否则的话, 有可能造成内存泄漏. 另外, session失效后会调用URLSession:​did​Become​Invalid​With​Error:方法, 之后session释放对代理的强引用.

Background Transport

需要注意的是, 在后台session中, 一些代理方法将失效. 下面说一些使用后台session的注意点 :

  • 后台session必须提供一个代理处理突发事件
  • 只支持HTTP(S)协议. 其他协议都不可用.
  • 只支持上传/下载任务, data任务不支持.
  • 后台任务有数量限制
  • 当任务数量到达系统指定的临界值的时候, 一些后台任务就会被取消. 也就是说, 一个需要长时间上传/下载的任务很可能会被系统取消然后有可能过一会再重新开始, 所以支持断点续传很重要.
  • 如果一个后台传输任务是在app在后台的时候开启的, 那么这个任务很可能会出于对性能的考虑随时被系统取消掉. . (相当于session的Configuration对象的discretionary属性为true.)

后台session限制确实很多, 所以尽可能使用前台session做事情.

Note

后台session最好用来传输一些支持断点续传大文件. 或对这个过程进行一些针对性的优化

  • 最好把文件先压缩成zip/tar等压缩文件再上传/下载.
  • 把大文件按数据段分别发送, 发送完之后服务端再把数据拼接起来.
  • 上传的时候服务端应该返回一个标识符, 这样可以追踪传输的状态, 及时做出传输的调整
  • 增加一个web代理服务器中间层, 以促进上述的优化

Usage

那么如何使用这个后台传输呢?

  • 创建一个后台session
    NSURLSessionConfiguration *config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"com.Jerry4me.backgroundSessionIdentifier"];
    _backgroundSession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];

  • 创建一个upload or download task

NSURL *URL = [NSURL URLWithString:@"http://www.bz55.com/uploads/allimg/140402/137-140402153504.jpg"];
NSURLRequest *request = [NSURLRequest requestWithURL:URL];
    
self.task = [self.session downloadTaskWithRequest:request];
/**注意 : 后台任务不能使用带有completionHandler的方法创建 **/
/**注意 : 如果任务只想在app进入后台后才处理, 那么可不调用[task resume]主动执行, 待程序进入后台后会自动执行 **/
  • 我们等下载到一半后进入后台, 打开App Switcher过一会可以发现, 图片下载完之后就会显示在应用程序上. 方法调用顺序为 : 下面四个方法全部都是app在后台时调用的
深入了解NSURLSession_第6张图片
2017-03-24 14:17:09.458415 JRBgSessionDemo[2766:1080861] 下载中 - 58%
2017-03-24 14:17:09.567957 JRBgSessionDemo[2766:1080861] 下载中 - 59%
2017-03-24 14:17:16.916830 JRBgSessionDemo[2766:1080828] -[AppDelegate application:handleEventsForBackgroundURLSession:completionHandler:]
2017-03-24 14:17:16.951185 JRBgSessionDemo[2766:1080977] -[DownloadViewController URLSession:downloadTask:didFinishDownloadingToURL:]
2017-03-24 14:17:16.953951 JRBgSessionDemo[2766:1080977] -[DownloadViewController URLSession:task:didCompleteWithError:]
2017-03-24 14:17:16.954574 JRBgSessionDemo[2766:1080977] -[DownloadViewController URLSessionDidFinishEventsForBackgroundURLSession:]

总结后台传输

  1. 尽量用真机进行调试, 模拟器会跳过某一两个方法
  2. 只能进行upload/download task, 不能进行data task
  3. 不能使用带completionHandler的方法创建task, 否则程序直接挂掉
  4. Applecation里的completionHandler必须存储起来, 等你处理完所有事情之后再调用告诉系统可以进行Snapshot和挂起app了
  5. 后台下载最好支持断点续传, 因为任务有可能会被系统主动取消(例如系统性能下降了, 资源不够用的情况下)

后台传输的Demo在文章头部的地方, 也可以点这里进去

NSURLSession API

  • 创建Session

    • + session​With​Configuration:​​ : 创建一个指定配置的session
    • + session​With​Configuration:​​delegate:​​delegate​Queue:​​ : 创建一个指定配置, 代理和代理方法执行队列的session
    • shared​Session : 返回session单例
  • 配置Session

    • configuration : 配置
    • delegate : 代理对象
    • delegateQueue : 代理方法的执行队列
    • sessionDescription : app定义的对于该session的描述
  • 添加data任务

    • - dataTaskWithURL: : 获取指定URL内容
    • - dataTaskWithURL:completionHandler: : 获取指定URL内容, 在completionHandler中处理数据. 该方法会绕过代理方法(除了身份认证挑战的代理方法)
    • - data​Task​With​Request:​​ : 获取指定URLRequest内容
    • - data​Task​With​Request:​​completionHandler: : 获取指定URLRequest内容, 在completionHandler中处理数据. 该方法会绕过代理方法(除了身份认证挑战的代理方法)
  • 添加download任务

    • - downloadTaskWithURL: : 下载指定URL内容
    • - downloadTaskWithURL:completionHandler: : 下载指定URL内容, 在completionHandler中处理数据. 该方法会绕过代理方法(除了身份认证挑战的代理方法)
    • - downloadTask​With​Request:​​ : 下载指定URLRequest内容
    • - downloadTask​With​Request:​​completionHandler: : 下载指定URLRequest内容, 在completionHandler中处理数据. 该方法会绕过代理方法(除了身份认证挑战的代理方法)
    • - downloadTask​With​ResumeData:​​ : 创建一个之前被取消/下载失败的download task
    • - downloadTask​With​ResumeData:​​completionHandler: : 创建一个之前被取消/下载失败的download task, 在completionHandler中处理数据. 该方法会绕过代理方法(除了身份认证挑战的代理方法)
  • 添加upload任务

    • - upload​Task​With​Request:​​from​Data:​​ : 通过HTTP请求发送data给指定URL
    • - upload​Task​With​Request:​​from​Data:completionHandler: : 通过HTTP请求发送data给指定URL, 在completionHandler中处理数据. 该方法会绕过代理方法(除了身份认证挑战的代理方法)
    • - upload​Task​With​Request:​​from​File:​​ : 通过HTTP请求发送指定文件给指定URL
    • - upload​Task​With​Request:​​from​File:completionHandler: : 通过HTTP请求发送指定文件给指定URL, 在completionHandler中处理数据. 该方法会绕过代理方法(除了身份认证挑战的代理方法)
    • upload​Task​With​StreamedRequest : 通过HTTP请求发送指定URLRequest数据流给指定URL
  • 添加stream任务

    • - streamTask​With​HostName:port:​​ : 通过给定的域名和端口建立双向TCP/IP连接
    • - streamTask​With​NetService: : 通过给定的network service建立双向TCP/IP连接
  • 管理session

    • finishTasksAndInvalidate : 任务全部完成后销毁session
    • flushWithCompletionHandler: : 清除硬盘上的cookies和证书, 清理暂时的缓存, 确保未来能响应一个新的TCP请求
    • getTasksWithCompletionHandler: : 异步调用session中所有upload, download, data tasks的completion回调.
    • invalidateAndCancel : 取消所有未完成的任务并销毁session
    • resetWithCompletionHandler: : 清空cookies, 缓存和证书存储, 移除所有磁盘文件, 清理正在执行的下载任务, 确保未来能响应一个新的socket请求

API总结

所有创建task的方法, 只要带有completionHandler这个参数的, 均表示为请求过程中不会触发代理方法. 所有不带有completionHandler这个参数的, 均会走代理方法流程.

如果你实现了URLSession:​did​Receive​Challenge:​completion​Handler:​方法又没有在该方法调用completionHandler, 请求就会遭到阻塞

断点续传

  • 下载失败/暂停/被取消, 可以通过task的- cancel​By​Producing​Resume​Data:​方法保存已下载的数据, 然后调用session的download​Task​With​Resume​Data:​方法, 触发代理的URLSession:​download​Task:​did​Resume​At​Offset:​expected​Total​Bytes:​方法

Something else Important

NSCopying Behavior

session, task和configuration对象都支持copy操作 :

  • session/task copy : 返回session对象本身
  • configuration copy : 返回一个无法修改(immutable)的对象.
线程安全

URLSession 的API全部都是线程安全的. 你可以在任何线程上创建session和tasks, task会自动调度到合适的代理队列中运行.

Warning

后台传输的代理方法URLSession​Did​Finish​Events​For​Background​URLSession:​可能会在其他线程中被调用. 在该方法中你应该回到主线程然后调用completion handler去触发AppDelegate中的application:​handle​Events​For​Background​URLSession:​completion​Handler:​方法.

常量
  • NSURLSession-Specific NSError user​Info Dictionary Keys : NSURLSession API 中出现的NSError的userInfo的keys
  • Background Task Cancellation reasons : 指示系统为什么取消了你的后台任务的理由
  • Transfer Size Constant : 指示一个未知传输大小的常量

参考文档

NSURLSession - Foundation
WWDC 2013 - Session 204 - What's New with Multitasking
WWDC 2013 - Session 705 - What's New in Foundation Networking
WWDC 2015 - Session 711 - Networking with NSURLSession
WWDC 2016 - Session 711 - NSURLSession: New Features and Best Practices

你可能感兴趣的:(深入了解NSURLSession)