Github : Jerry4me, Demo : JRBgSessionDemo
前言
本文主要是結(jié)合官方文檔, 挖掘NSURLSession的類層次結(jié)構(gòu)及其聯(lián)系, 總結(jié)出關(guān)于NSURLSession的一些關(guān)鍵點及其用法.
關(guān)于NSURLSession為什么能取代NSURLConnection, 其優(yōu)勢是什么, 及其NSURLSession API的概述, 見
關(guān)于ATS, HTTP/2, 以及iOS9 NSURLSession新特性 : sharedCookies, streamTask和taskMetrics, 見
以上兩篇文章都是我看wwdc視頻然后總結(jié)出來的文章, 大家感興趣的可以先了解了解. 如果不想知道那么多, 只想知道怎么用NSURLSession, 那就直接看本文的正文.
*了解URL Loading System

目錄
-
介紹NSURLSession相關(guān)類
-
身份驗證和自定義TLS
-
[App Transport Security](#App Transport Security)
-
[NSURLSession 工作流程](#NSURLSession 工作流程)
-
[后臺傳輸及其用法](#Background Transport)
-
[NSURLSession API](#NSURLSession API)
-
[其他一些注意點](#Something else Important)
NSURLSession
NSURLSession
- 支持data, ftp, http(s)協(xié)議, 同時支持代理服務(wù)器和socks網(wǎng)關(guān).
- 支持http/1.1, http/2, spdy協(xié)議, 但同時需要服務(wù)器支持ALPN和NPN.
ALPN與NPN
- ALPN(Application Layer Protocol Negotiation,應(yīng)用層協(xié)議協(xié)商)
- NPN(Next Protocol Negotiation,下一代協(xié)議協(xié)商)
NPN是服務(wù)端發(fā)送它支持的HTTP協(xié)議列表, 供客戶端選擇; 而ALPN則相反, 由客戶端發(fā)送它支持的HTTP協(xié)議列表, 供服務(wù)端選擇. 如果缺少NPN/ALPN其中一個, 則無法使用HTTP/2通信. 具體請見為什么我們應(yīng)該盡快支持 ALPN.
NSURLSession相關(guān)類為 :
- NSURLSession
- NSURLSessionConfiguration
- NSURLSessionDelegate
- NSURLSessionTask
- NSURLSessionTaskMetrics
- NSURLSessionTaskTransactionMetrics
他們相互的關(guān)系如下 :

NSURLSession
session分為 :
- 全局共享單例session :
NSURLSession sharedSession, 有一定的局限性 - 自定義session : 自定義配置文件, 設(shè)置代理, 大部分時間我們都是用這個
- 后臺session : 也是自定義session的一種, 只是他專門用于做后臺上傳/下載任務(wù)
session為哪一種類型完全由其內(nèi)部的Configuration而定.
NSURLSessionConfiguration
配置分為 :
- defaultSessionConfiguration : 系統(tǒng)默認
- ephemeralSessionConfiguration : 僅內(nèi)存緩存, 不做磁盤緩存的配置
- backgroundSessionConfiguration : 這里需要指定一個identifier, identifier用來后臺重連session對象. (做后臺上傳/下載就是這個config)
另外, 我們還可以給Configuration對象再自定義一些屬性, 例如每端口的最大并發(fā)HTTP請求數(shù)目, 以及是否允許蜂窩網(wǎng)絡(luò), 請求緩存策略, 請求超時, cookies/證書存儲策略等等
NSURLSessionDelegate

session管理的一組tasks共享一個代理, 不想實現(xiàn)代理方法時, 代理傳nil即可.
代理協(xié)議分為 :
-
NSURLSessionDelegate: session-level的代理方法 -
NSURLSessionTaskDelegate: task-level面向all的代理方法 -
NSURLSessionDataDelegate: task-level面向data和upload的代理方法 -
NSURLSession?Download?Delegate: task-level的面向download的代理方法 -
NSURLSessionStreamDelegate: task-level的面向stream的代理方法
NSURLSessionTask

session task類型分為 :
-
NSURLSessionTask: Task的抽象基類 -
NSURLSessionDataTask: 以NSData的形式接收一個URLRequest的內(nèi)容 -
NSURLSessionUploadTask: 上傳NSData或者本地磁盤中的文件, 完成后以NSData的形式接收一個URLRequest的響應(yīng) -
NSURLSessionDownloadTask: 下載完成后返回臨時文件在本地磁盤的URL路徑 -
NSURLSessionStreamTask: 用于建立一個TCP/IP連接
NSURLSessionTaskMetrics 和 NSURLSessionTaskTransactionMetrics
對發(fā)送請求/DNS查詢/TLS握手/請求響應(yīng)等各種環(huán)節(jié)時間上的統(tǒng)計. 更易于我們檢測, 分析我們App的請求緩慢到底是發(fā)生在哪個環(huán)節(jié), 并對此進行優(yōu)化提升我們APP的性能.
NSURLSessionTaskMetrics對象與NSURLSessionTask對象一一對應(yīng). 每個NSURLSessionTaskMetrics對象內(nèi)有3個屬性 :
- taskInterval : task從開始到結(jié)束總共用的時間
- redirectCount : task重定向的次數(shù)
- transactionMetrics : 一個task從發(fā)出請求到收到數(shù)據(jù)過程中派生出的每個子請求, 它是一個裝著許多NSURLSessionTaskTransactionMetrics對象的數(shù)組. 每個對象都代表下圖的一個子過程

API很簡單, 就一個方法 : - (void)URLSession: task: didFinishCollectingMetrics:, 當(dāng)收集完成的時候就會調(diào)用該方法.
身份驗證和自定義TLS
當(dāng)一個服務(wù)器請求身份驗證或TLS握手期間需要提供證書的話, URLSession會調(diào)用他的代理方法
URLSession:?did?Receive?Challenge:?completion?Handler:?去處理.如果你沒有實現(xiàn)該代理方法, URLSession就會這么做 :
- 使用身份認證信息作為請求URL的一部分(如果可用的話)
- 在用戶的keychain中查找網(wǎng)絡(luò)密碼和證書(in macOS), 在app的keychain中查找(in iOS)
- 如果證書還是不可用或服務(wù)器拒絕該證書, 就會繼續(xù)缺少身份認證的連接.
- 對于HTTP(S)連接, 請求失敗并返回一個狀態(tài)碼, 可能會提供一些替代的內(nèi)容, 例如一個私人網(wǎng)站的公共網(wǎng)頁.
- 對于其他URL類型(如FTP等), 則連接請求失敗, 直接返回錯誤信息
App Transport Security
從iOS9開始支持ATS, 且默認ATS只支持發(fā)送HTTPS請求, 不允許發(fā)送不安全的HTTP請求. 如果用戶需要發(fā)送HTTP請求需要在info.plist中配置一些東西.
詳情在文章開頭的iOS9 ATS HTTP/2 NSURLSession中說得很詳細, 想了解的可以進去閱讀.
NSURLSession 工作流程
那么如何使用NSURLSession像從前用NSURLConnection那樣發(fā)送一個請求呢?
// 設(shè)置配置
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
/** 設(shè)置其他配置屬性 **/
// 代理隊列
NSOperationQueue *queue = [NSOperationQueue mainQueue];
// 創(chuàng)建session
NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:queue];
// 利用session創(chuàng)建n個task
NSURLSessionDownloadTask *task = [session downloadTaskWithURL:[NSURL URLWithString:@"https://www.baidu.com"]];
// 開始
[task resume];
然后就可以在代理方法中處理各種事情了. 簡單吧? 下面分task說明代理方法的調(diào)用情況..
身份驗證或TLS握手
這是所有task都必須經(jīng)歷的一個過程. 當(dāng)一個服務(wù)器請求身份驗證或TLS握手期間需要提供證書的話, URLSession會調(diào)用他的代理方法URLSession:?did?Receive?Challenge:?completion?Handler:?去處理., 另外, 如果連接途中收到服務(wù)器返回需要身份認證的response, 也會調(diào)用該代理方法.
重定位response
這也是所有task都有可能經(jīng)歷的一個過程, 如果response是HTTP重定位, session會調(diào)用代理的URLSession:?task:?will?Perform?HTTPRedirection:?new?Request:?completion?Handler:方法. 這里需要調(diào)用completionHandler告訴session是否允許重定位, 或者重定位到另一個URL, 或者傳nil表示重定位的響應(yīng)body有效并返回. 如果代理沒有實現(xiàn)該方法, 則允許重定位直到達到最大重定位次數(shù).
DataTask
- 對于一個data task來說, session會調(diào)用代理的
URLSession:?data?Task:?did?Receive?Response:?completion?Handler:?方法, 決定是否將一個data dask轉(zhuǎn)換成download task, 然后調(diào)用completion回調(diào)繼續(xù)接收data或下載data.
- 如果你的app選擇轉(zhuǎn)換成download task, session會調(diào)用代理的
URLSession:?data?Task:?did?Become?Download?Task:?方法并把新的download task對象以方法參數(shù)的形式傳給你. 之后代理不會再收到data task的回調(diào)而是轉(zhuǎn)為收到download task的回調(diào)
在服務(wù)器傳輸數(shù)據(jù)給客戶端期間, 代理會周期性地收到
URLSession:?data?Task:?did?Receive?Data:?回調(diào)session會調(diào)用
URLSession:?data?Task:?will?Cache?Response:?completion?Handler:?詢問你的app是否允許緩存. 如果代理不實現(xiàn)這個方法的話, 默認使用session綁定的Configuration的緩存策略.
DownloadTask
對于一個通過
download?Task?With?Resume?Data:?創(chuàng)建的下載任務(wù), session會調(diào)用代理的URLSession:?download?Task:?did?Resume?At?Offset:?expected?Total?Bytes:?方法.在服務(wù)器傳輸數(shù)據(jù)給客戶端期間, 調(diào)用
URLSession:?download?Task:?did?Write?Data:?total?Bytes?Written:?total?Bytes?Expected?To?Write:給用戶傳數(shù)據(jù)
- 當(dāng)用戶暫停下載時, 調(diào)用
cancel?By?Producing?Resume?Data:?給用戶傳已下好的數(shù)據(jù). - 如果用戶想要恢復(fù)下載, 把剛剛的resumeData以參數(shù)的形式傳給
download?Task?With?Resume?Data:?方法創(chuàng)建新的task繼續(xù)下載.
- 如果download task成功完成了, 調(diào)用
URLSession:?download?Task:?did?Finish?Downloading?To?URL:把臨時文件的URL路徑給你. 此時你應(yīng)該在該代理方法返回以前讀取他的數(shù)據(jù)或者把文件持久化.
UploadTask
上傳數(shù)據(jù)去服務(wù)器期間, 代理會周期性收到URLSession:?task:?did?Send?Body?Data:?total?Bytes?Sent:?total?Bytes?Expected?To?Send:回調(diào)并獲得上傳進度的報告.
StreamTask
如果任務(wù)的數(shù)據(jù)是由一個stream發(fā)出的, session就會調(diào)用代理的URLSession:?task:?need?New?Body?Stream:?方法去獲取一個NSInputStream對象并提供一個新請求的body data.
task completion
任何task完成的時候, 都會調(diào)用URLSession:?task:?did?Complete?With?Error:?方法, error有可能為nil(請求成功), 不為nil(請求失敗)
請求失敗, 但是該任務(wù)是可恢復(fù)下載的, 那么error對象的userInfo字典里有一個
NSURLSession?Download?Task?Resume?Data對應(yīng)的value, 你應(yīng)該把這個值傳給download?Task?With?Resume?Data:?方法重新恢復(fù)下載請求失敗, 但是任務(wù)無法恢復(fù)下載, 那么應(yīng)該重新創(chuàng)建一個下載任務(wù)并從頭開始下載.
因為其他原因(如服務(wù)器錯誤等等), 創(chuàng)建并恢復(fù)請求.
Note
NSURLSession不會收到服務(wù)器傳來的錯誤, 代理只會收到客戶端出現(xiàn)的錯誤, 例如無法解析主機名或無法連接上主機等等. 客戶端錯誤定義在URL Loading System Error Codes. 服務(wù)端錯誤通過HTTP狀態(tài)法進行傳輸, 詳情請看NSHTTPURLResponse和NSURLResponse類.
銷毀session
如果你不再需要一個session了, 一定要調(diào)用它的invalidateAndCancel或finishTasksAndInvalidate方法. (前者是取消所有未完成的任務(wù)然后使session失效, 后者是等待正在執(zhí)行的任務(wù)完成之后再使session失效). 否則的話, 有可能造成內(nèi)存泄漏. 另外, session失效后會調(diào)用URLSession:?did?Become?Invalid?With?Error:方法, 之后session釋放對代理的強引用.
Background Transport
需要注意的是, 在后臺session中, 一些代理方法將失效. 下面說一些使用后臺session的注意點 :
- 后臺session必須提供一個代理處理突發(fā)事件
- 只支持HTTP(S)協(xié)議. 其他協(xié)議都不可用.
- 只支持上傳/下載任務(wù), data任務(wù)不支持.
- 后臺任務(wù)有數(shù)量限制
- 當(dāng)任務(wù)數(shù)量到達系統(tǒng)指定的臨界值的時候, 一些后臺任務(wù)就會被取消. 也就是說, 一個需要長時間上傳/下載的任務(wù)很可能會被系統(tǒng)取消然后有可能過一會再重新開始, 所以支持斷點續(xù)傳很重要.
- 如果一個后臺傳輸任務(wù)是在app在后臺的時候開啟的, 那么這個任務(wù)很可能會出于對性能的考慮隨時被系統(tǒng)取消掉. . (相當(dāng)于session的Configuration對象的discretionary屬性為true.)
后臺session限制確實很多, 所以盡可能使用前臺session做事情.
Note
后臺session最好用來傳輸一些支持斷點續(xù)傳大文件. 或?qū)@個過程進行一些針對性的優(yōu)化
- 最好把文件先壓縮成zip/tar等壓縮文件再上傳/下載.
- 把大文件按數(shù)據(jù)段分別發(fā)送, 發(fā)送完之后服務(wù)端再把數(shù)據(jù)拼接起來.
- 上傳的時候服務(wù)端應(yīng)該返回一個標(biāo)識符, 這樣可以追蹤傳輸?shù)臓顟B(tài), 及時做出傳輸?shù)恼{(diào)整
- 增加一個web代理服務(wù)器中間層, 以促進上述的優(yōu)化
Usage
那么如何使用這個后臺傳輸呢?
創(chuàng)建一個后臺session
NSURLSessionConfiguration *config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"com.Jerry4me.backgroundSessionIdentifier"];
_backgroundSession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];創(chuàng)建一個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];
/**注意 : 后臺任務(wù)不能使用帶有completionHandler的方法創(chuàng)建 **/
/**注意 : 如果任務(wù)只想在app進入后臺后才處理, 那么可不調(diào)用[task resume]主動執(zhí)行, 待程序進入后臺后會自動執(zhí)行 **/
- 我們等下載到一半后進入后臺, 打開App Switcher過一會可以發(fā)現(xiàn), 圖片下載完之后就會顯示在應(yīng)用程序上. 方法調(diào)用順序為 : 下面四個方法全部都是app在后臺時調(diào)用的

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:]
總結(jié)后臺傳輸
- 盡量用真機進行調(diào)試, 模擬器會跳過某一兩個方法
- 只能進行upload/download task, 不能進行data task
- 不能使用帶completionHandler的方法創(chuàng)建task, 否則程序直接掛掉
- Applecation里的completionHandler必須存儲起來, 等你處理完所有事情之后再調(diào)用告訴系統(tǒng)可以進行Snapshot和掛起app了
- 后臺下載最好支持斷點續(xù)傳, 因為任務(wù)有可能會被系統(tǒng)主動取消(例如系統(tǒng)性能下降了, 資源不夠用的情況下)
后臺傳輸?shù)腄emo在文章頭部的地方, 也可以點這里進去
NSURLSession API
-
創(chuàng)建Session
-
+ session?With?Configuration:??: 創(chuàng)建一個指定配置的session -
+ session?With?Configuration:??delegate:??delegate?Queue:??: 創(chuàng)建一個指定配置, 代理和代理方法執(zhí)行隊列的session -
shared?Session: 返回session單例
-
-
配置Session
-
configuration: 配置 -
delegate: 代理對象 -
delegateQueue: 代理方法的執(zhí)行隊列 -
sessionDescription: app定義的對于該session的描述
-
-
添加data任務(wù)
-
- dataTaskWithURL:: 獲取指定URL內(nèi)容 -
- dataTaskWithURL:completionHandler:: 獲取指定URL內(nèi)容, 在completionHandler中處理數(shù)據(jù). 該方法會繞過代理方法(除了身份認證挑戰(zhàn)的代理方法) -
- data?Task?With?Request:??: 獲取指定URLRequest內(nèi)容 -
- data?Task?With?Request:??completionHandler:: 獲取指定URLRequest內(nèi)容, 在completionHandler中處理數(shù)據(jù). 該方法會繞過代理方法(除了身份認證挑戰(zhàn)的代理方法)
-
-
添加download任務(wù)
-
- downloadTaskWithURL:: 下載指定URL內(nèi)容 -
- downloadTaskWithURL:completionHandler:: 下載指定URL內(nèi)容, 在completionHandler中處理數(shù)據(jù). 該方法會繞過代理方法(除了身份認證挑戰(zhàn)的代理方法) -
- downloadTask?With?Request:??: 下載指定URLRequest內(nèi)容 -
- downloadTask?With?Request:??completionHandler:: 下載指定URLRequest內(nèi)容, 在completionHandler中處理數(shù)據(jù). 該方法會繞過代理方法(除了身份認證挑戰(zhàn)的代理方法) -
- downloadTask?With?ResumeData:??: 創(chuàng)建一個之前被取消/下載失敗的download task -
- downloadTask?With?ResumeData:??completionHandler:: 創(chuàng)建一個之前被取消/下載失敗的download task, 在completionHandler中處理數(shù)據(jù). 該方法會繞過代理方法(除了身份認證挑戰(zhàn)的代理方法)
-
-
添加upload任務(wù)
-
- upload?Task?With?Request:??from?Data:??: 通過HTTP請求發(fā)送data給指定URL -
- upload?Task?With?Request:??from?Data:completionHandler:: 通過HTTP請求發(fā)送data給指定URL, 在completionHandler中處理數(shù)據(jù). 該方法會繞過代理方法(除了身份認證挑戰(zhàn)的代理方法) -
- upload?Task?With?Request:??from?File:??: 通過HTTP請求發(fā)送指定文件給指定URL -
- upload?Task?With?Request:??from?File:completionHandler:: 通過HTTP請求發(fā)送指定文件給指定URL, 在completionHandler中處理數(shù)據(jù). 該方法會繞過代理方法(除了身份認證挑戰(zhàn)的代理方法) -
upload?Task?With?StreamedRequest: 通過HTTP請求發(fā)送指定URLRequest數(shù)據(jù)流給指定URL
-
-
添加stream任務(wù)
-
- streamTask?With?HostName:port:??: 通過給定的域名和端口建立雙向TCP/IP連接 -
- streamTask?With?NetService:: 通過給定的network service建立雙向TCP/IP連接
-
-
管理session
-
finishTasksAndInvalidate: 任務(wù)全部完成后銷毀session -
flushWithCompletionHandler:: 清除硬盤上的cookies和證書, 清理暫時的緩存, 確保未來能響應(yīng)一個新的TCP請求 -
getTasksWithCompletionHandler:: 異步調(diào)用session中所有upload, download, data tasks的completion回調(diào). -
invalidateAndCancel: 取消所有未完成的任務(wù)并銷毀session -
resetWithCompletionHandler:: 清空cookies, 緩存和證書存儲, 移除所有磁盤文件, 清理正在執(zhí)行的下載任務(wù), 確保未來能響應(yīng)一個新的socket請求
-
API總結(jié)
所有創(chuàng)建task的方法, 只要帶有completionHandler這個參數(shù)的, 均表示為請求過程中不會觸發(fā)代理方法. 所有不帶有completionHandler這個參數(shù)的, 均會走代理方法流程.
如果你實現(xiàn)了URLSession:?did?Receive?Challenge:?completion?Handler:?方法又沒有在該方法調(diào)用completionHandler, 請求就會遭到阻塞
斷點續(xù)傳
- 下載失敗/暫停/被取消, 可以通過task的
- cancel?By?Producing?Resume?Data:?方法保存已下載的數(shù)據(jù), 然后調(diào)用session的download?Task?With?Resume?Data:?方法, 觸發(fā)代理的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全部都是線程安全的. 你可以在任何線程上創(chuàng)建session和tasks, task會自動調(diào)度到合適的代理隊列中運行.
Warning
后臺傳輸?shù)拇矸椒?code>URLSession?Did?Finish?Events?For?Background?URLSession:?可能會在其他線程中被調(diào)用. 在該方法中你應(yīng)該回到主線程然后調(diào)用completion handler去觸發(fā)AppDelegate中的
application:?handle?Events?For?Background?URLSession:?completion?Handler:?方法.
常量
-
NSURLSession-Specific NSError user?Info Dictionary Keys: NSURLSession API 中出現(xiàn)的NSError的userInfo的keys -
Background Task Cancellation reasons: 指示系統(tǒng)為什么取消了你的后臺任務(wù)的理由 -
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