深入了解NSURLSession

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

  1. 當(dāng)一個服務(wù)器請求身份驗證或TLS握手期間需要提供證書的話, URLSession會調(diào)用他的代理方法URLSession:?did?Receive?Challenge:?completion?Handler:?去處理.

  2. 如果你沒有實現(xiàn)該代理方法, URLSession就會這么做 :

  • 使用身份認證信息作為請求URL的一部分(如果可用的話)
  • 在用戶的keychain中查找網(wǎng)絡(luò)密碼和證書(in macOS), 在app的keychain中查找(in iOS)
  1. 如果證書還是不可用或服務(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

  1. 對于一個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)
  1. 在服務(wù)器傳輸數(shù)據(jù)給客戶端期間, 代理會周期性地收到URLSession:?data?Task:?did?Receive?Data:?回調(diào)

  2. session會調(diào)用URLSession:?data?Task:?will?Cache?Response:?completion?Handler:?詢問你的app是否允許緩存. 如果代理不實現(xiàn)這個方法的話, 默認使用session綁定的Configuration的緩存策略.

DownloadTask

  1. 對于一個通過download?Task?With?Resume?Data:?創(chuàng)建的下載任務(wù), session會調(diào)用代理的URLSession:?download?Task:?did?Resume?At?Offset:?expected?Total?Bytes:?方法.

  2. 在服務(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ù)下載.
  1. 如果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)用它的invalidateAndCancelfinishTasksAndInvalidate方法. (前者是取消所有未完成的任務(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é)后臺傳輸

  1. 盡量用真機進行調(diào)試, 模擬器會跳過某一兩個方法
  2. 只能進行upload/download task, 不能進行data task
  3. 不能使用帶completionHandler的方法創(chuàng)建task, 否則程序直接掛掉
  4. Applecation里的completionHandler必須存儲起來, 等你處理完所有事情之后再調(diào)用告訴系統(tǒng)可以進行Snapshot和掛起app了
  5. 后臺下載最好支持斷點續(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容