這篇文章介紹了 URL Loading System 相關(guān)知識(shí),涉及以下內(nèi)容:
-
URLSession類型。 -
URLSessionTask類型 -
URLSessionDelegate、URLSessionTaskDelegate、URLSessionDataDelegate、URLSessionDownloadDelegate四種協(xié)議。 - 使用
URLSessionDataTask請(qǐng)求數(shù)據(jù)。 - 使用
URLSessionDownloadTask下載、暫停、恢復(fù)下載視頻。支持?jǐn)帱c(diǎn)續(xù)傳、后臺(tái)下載,下載完成后自動(dòng)保存到相冊(cè)。

URL 加載系統(tǒng)(URL Loading System)使用標(biāo)準(zhǔn)協(xié)議(如 https)或自定義協(xié)議提供對(duì) URL 標(biāo)識(shí)資源進(jìn)行訪問(wèn)。URL Loading System 是異步執(zhí)行的,這樣 app 可以保持響應(yīng),并在 response 到達(dá)時(shí)處理數(shù)據(jù)或錯(cuò)誤。
使用URLSession實(shí)例創(chuàng)建一個(gè)或多個(gè)URLSessionTask實(shí)例,URLSessionTask實(shí)例可以拉取數(shù)據(jù)并將數(shù)據(jù)返回到 app、下載文件,或?qū)⑽募?shù)據(jù)上傳到遠(yuǎn)程服務(wù)器。使用URLSessionConfiguration對(duì)象配置URLSession的實(shí)例 session(會(huì)話),URLSessionConfiguration對(duì)象可以配置 caches、cookies 策略,以及是否允許使用數(shù)據(jù)流量等。
可以使用一個(gè) session 重復(fù)創(chuàng)建 task。例如,瀏覽器為正常瀏覽和無(wú)痕模式使用單獨(dú)的 session,無(wú)痕瀏覽不會(huì)保存數(shù)據(jù)到磁盤(pán)。下圖顯示了具有這些配置的兩個(gè) session 如何創(chuàng)建多個(gè) task:

每個(gè) session 關(guān)聯(lián)一個(gè) delegate 以接收定期更新或錯(cuò)誤。默認(rèn)情況下,delegate 調(diào)用完成處理程序塊;如果提供了自定義的 delegate,則不會(huì)調(diào)用完成處理程序塊。
可以將 session 配置為后臺(tái)會(huì)話,以便在 app 處于非活躍狀態(tài)時(shí)繼續(xù)下載數(shù)據(jù),下載完成后喚醒 app 并提供結(jié)果。
1. URLSession
配置和創(chuàng)建 session,使用 session 創(chuàng)建 task 并與 URL 交互。
URLSession和相關(guān)類提供的 API 可以從指定 URL 下載,或上傳數(shù)據(jù)到指定 URL。該 API 允許 app 未運(yùn)行時(shí)執(zhí)行后臺(tái)下載。在 iOS 中,允許 app 處于 suspend 狀態(tài)時(shí)繼續(xù)下載。另外,還提供了一組豐富的委托方法以支持身份驗(yàn)證、重定向通知等。
通過(guò)URLSession API,可以創(chuàng)建一個(gè)或多個(gè) session,每個(gè) session 協(xié)調(diào)一組數(shù)據(jù)傳輸任務(wù)。例如,如果你在開(kāi)發(fā)瀏覽器,可以為每個(gè)標(biāo)簽或窗口創(chuàng)建一個(gè)會(huì)話,也可以一個(gè) session 用于交互、一個(gè) session 用于后臺(tái)下載。app 向一個(gè) session 添加一系列 task,每個(gè)任務(wù)代表一個(gè)指向特定 URL 的 request。
2. URLSessionConfiguration
URLSessionConfiguration對(duì)象定義了使用URLSession上傳、下載時(shí)要使用的行為和策略。使用URLSession時(shí)要先創(chuàng)建URLSessionConfiguration。URLSessionConfiguration對(duì)象定義了單個(gè)主機(jī)最大同時(shí)連接數(shù)、是否允許通過(guò)蜂窩網(wǎng)絡(luò)進(jìn)行連接、超時(shí)時(shí)長(zhǎng)和緩存策略等。
在使用配置初始化會(huì)話前,必須配置好URLSessionConfiguration對(duì)象。使用URLSessionConfiguration初始化會(huì)話時(shí),session 會(huì)復(fù)制一份URLSessionConfiguration對(duì)象。一旦配置完成,session 將忽略任務(wù)對(duì)URLSessionConfiguration對(duì)象的修改。如果需要改變傳輸策略,需要更新 session configuration 對(duì)象,并用更新后的 session configuration 創(chuàng)建一個(gè)新的 session。
某些情況下,configuration 指定的策略會(huì)被任務(wù)的
URLRequest對(duì)象重寫(xiě)。默認(rèn)采用 request 指定的策略,除非 session 的策略更為嚴(yán)格。例如,sesseion configuration 指定禁止使用蜂窩網(wǎng)絡(luò),則URLRequest對(duì)象不能使用蜂窩網(wǎng)絡(luò)進(jìn)行請(qǐng)求。
URL session 的行為和能力很大程度上取決于創(chuàng)建會(huì)話的配置。
單例會(huì)話(singleton shared session)沒(méi)有配置對(duì)象,一般用于基本請(qǐng)求。單例會(huì)話不能像自己創(chuàng)建的會(huì)話一樣進(jìn)行配置,但如果需求非常有限,其是一個(gè)很好的起點(diǎn)。通過(guò)調(diào)用shared類方法獲取單例會(huì)話。
2.1 default
Default session 和 shared session 類似,但允許自定義配置,且可以通過(guò) delegate 獲取增量數(shù)據(jù);默認(rèn)使用基于磁盤(pán)的持久緩存(下載文件除外),并將憑據(jù)(credential)保存到用戶 keychain,還會(huì)將 cookie 保存到共享的 cookie store。通過(guò)調(diào)用URLSessionConfiguration類的default方法創(chuàng)建 default session 配置。
2.2 ephemeral
Ephemeral session 與 shared session 類似,但會(huì)將 cache、cookie 和 credential 等會(huì)話相關(guān)數(shù)據(jù)保存到 RAM,而非寫(xiě)入磁盤(pán)。只有在告訴會(huì)話將數(shù)據(jù)寫(xiě)入文件時(shí),ephemeral 類型會(huì)話才會(huì)將數(shù)據(jù)寫(xiě)入磁盤(pán)。通過(guò)調(diào)用NSURLSessionConfiguration類的ephemeral方法創(chuàng)建 ephemeral session 配置。
也可以自定義 default configuration 以獲得與 ephemeral configuration 相同的功能,但直接使用 ephemeral configuration 更為方便。
使用 ephemeral session 的主要優(yōu)點(diǎn)在于保護(hù)隱私。通過(guò)將敏感數(shù)據(jù)保存到 RAM 取代寫(xiě)入磁盤(pán),避免數(shù)據(jù)被攔截、它用。因此,ephemeral session 非常適合瀏覽器無(wú)痕瀏覽模式。
由于 ephemeral session 不會(huì)將緩存數(shù)據(jù)保存到磁盤(pán),緩存大小會(huì)受限于可用 RAM。這一限制決定了可緩存數(shù)據(jù)大小,也會(huì)影響 app 性能。用戶退出并重新啟動(dòng) app,所有緩存會(huì)被清空。
App 使會(huì)話無(wú)效時(shí),將自動(dòng)清除所有臨時(shí)會(huì)話數(shù)據(jù)。此外,在 iOS 中,app 處于 suspend 狀態(tài)時(shí)緩存不會(huì)被清空;app 終止或內(nèi)存不足時(shí),可能會(huì)清空緩存數(shù)據(jù)。
2.3 background
Background session 允許 app 未活躍時(shí)執(zhí)行 HTTP 和 HTTPS 上傳、下載任務(wù)。Background session 將下載任務(wù)提交給系統(tǒng),下載會(huì)在單獨(dú)進(jìn)程執(zhí)行。
通過(guò)調(diào)用URLSessionConfiguration類的backgroundSessionConfiguration(_:)方法創(chuàng)建 background session,Session identifier 需要在 app 內(nèi)唯一。
iOS app 被系統(tǒng)終止并再次啟動(dòng)后,app 可以使用同一 identifier 創(chuàng)建 configuration、session,用來(lái)獲取 app 終止時(shí)數(shù)據(jù)傳輸進(jìn)度,但只適用于系統(tǒng)終止 app 運(yùn)行;如果用戶從多任務(wù)中心終止 app,系統(tǒng)會(huì)取消該 app 的所有后臺(tái)任務(wù),且不會(huì)自動(dòng)喚醒應(yīng)用。用戶手動(dòng)打開(kāi) app 后才可以進(jìn)行傳輸任務(wù)。
3. URLSessionTask
URLSessionTask類是 URL 會(huì)話任務(wù)的基類,task 始終是 session 的一部分。URLSessionTask共有以下四個(gè)具體類:
URLSessionDataTask:使用dataTask(with:)方法創(chuàng)建URLSessionDataTask實(shí)例,data task 用于請(qǐng)求資源,將服務(wù)器的響應(yīng)作為一個(gè)或多個(gè)NSData對(duì)象返回到內(nèi)存中。Default、ephemeral、shared session 支持URLSessionDataTask,background session 不支持URLSessionDataTask。-
URLSessionUploadTask:使用uploadTask(with:from:)方法創(chuàng)建URLSessionUploadTask實(shí)例,URLSessionUploadTask繼承自URLSessionDataTask。使用URLSessionUploadTask可以很方便為 request 提供 body(例如,POST 或 PUT),還可以在收到 response 前上傳數(shù)據(jù)。此外,upload task 支持后臺(tái)會(huì)話。在 iOS 中,為 background session 創(chuàng)建 upload task 時(shí),系統(tǒng)會(huì)將文件復(fù)制到臨時(shí)目錄,然后從臨時(shí)目錄上傳。
URLSessionDownloadTask:使用downloadTask(with:)方法創(chuàng)建URLSessionDownloadTask實(shí)例,download task 將資源直接下載到磁盤(pán)上的文件。Download task 支持任何類型的會(huì)話。URLSessionStreamTask:使用streamTask(withHostName:port:)或streamTask(with:)方法創(chuàng)建URLSessionStreamTask實(shí)例。流任務(wù)(stream task)從主機(jī)、端口或網(wǎng)絡(luò)服務(wù)建立 TCP/IP連接。
創(chuàng)建任務(wù)后,調(diào)用resume()方法啟動(dòng)任務(wù)。在任務(wù)完成或失敗前,session 會(huì)強(qiáng)引用 task。如果沒(méi)有特別用途,不需要維護(hù)對(duì)任務(wù)的引用。
Task 還有
progress、countOfBytesReceived、currentRequest、response等屬性,且所有屬性支持KVO。如果你對(duì)觀察者還不熟悉,可以查看我的另一篇文章:KVC和KVO學(xué)習(xí)筆記
4. URLSessionDelegate
URLSessionDelegate協(xié)議定義了 URL session 實(shí)例調(diào)用 delegate 處理 session 級(jí)事件的方法。例如,session 生命周期改變。
除實(shí)現(xiàn)URLSessionDelegate協(xié)議內(nèi)方法,大部分 delegate 還需要實(shí)現(xiàn)URLSessionTaskDelegate、URLSessionDataDelegate、URLSessionDownloadDelegate中的一個(gè)或多個(gè)協(xié)議,以便處理 task 級(jí)事件,例如,task 開(kāi)始或結(jié)束,data task、download task 定期進(jìn)度更新。
urlSession(_:didBecomeInvalidWithError:)方法用以通知 URL session 該 session 已失效。如果通過(guò)調(diào)用finishTasksAndInvalidate()方法使會(huì)話無(wú)效,會(huì)話會(huì)在最后一個(gè) task 完成或失敗后調(diào)用該方法;如果通過(guò)調(diào)用invalidateAndCancel()方法使會(huì)話無(wú)效,會(huì)話立即調(diào)用該方法。
urlSession(_:didReceive:completionHandler:)方法響應(yīng)來(lái)自遠(yuǎn)程服務(wù)器的會(huì)話級(jí)身份驗(yàn)證請(qǐng)求。遇到以下兩種情況時(shí)會(huì)調(diào)用該方法:
- 遠(yuǎn)程服務(wù)器請(qǐng)求客戶端證書(shū),或 Windows NT LAN Manager(NTLM)認(rèn)證時(shí)會(huì)調(diào)用該方法以提供適當(dāng)?shù)膽{據(jù)。
- 當(dāng) session 與使用 SSL 或 TLS 的遠(yuǎn)程服務(wù)器首次建立連接時(shí),使用該方法驗(yàn)證服務(wù)器的證書(shū)鏈。
如果未實(shí)現(xiàn)該方法,session 會(huì)調(diào)用URLSessionTaskDelegate協(xié)議中urlSession(_:task:didReceive:completionHandler:)方法,采用 task 級(jí)認(rèn)證。
5. URLSessionTaskDelegate
URLSessionTaskDelegate協(xié)議定義了 URL Session 實(shí)例調(diào)用 delegate 處理 task 級(jí)事件的方法。URLSessionTaskDelegate繼承自URLSessionDelegate。
如果你在使用 download task,同時(shí)需要實(shí)現(xiàn)URLSessionDownloadDelegate協(xié)議內(nèi)方法;如果你在使用data task 或 upload task,同時(shí)需要實(shí)現(xiàn)URLSessionDataDelegate協(xié)議內(nèi)方法。
5.1 處理 task 任務(wù)生命周期變化
Task 數(shù)據(jù)傳輸完成時(shí)會(huì)調(diào)用urlSession(_:task:didCompleteWithError:)方法,如果發(fā)生錯(cuò)誤,則 error 參數(shù)會(huì)包含失敗原因。Error 參數(shù)不包含服務(wù)端錯(cuò)誤,只包含客戶端錯(cuò)誤。例如無(wú)法解析主機(jī)名、無(wú)法連接主機(jī)。
5.2 處理重定向
遠(yuǎn)程服務(wù)器請(qǐng)求 HTTP 重定向時(shí)會(huì)調(diào)用urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:)方法。只有 default session 和 ephemeral session 中的 task 會(huì)調(diào)用該方法,background session 中 task 會(huì)直接重定向。
在該方法內(nèi)必須調(diào)用 completion handler。如果允許重定向,為 completion handler 傳入 request 參數(shù);如果需要修改重定向,傳入修改后的 request 對(duì)象;如果禁止重定向,則參數(shù)傳 nil,此時(shí)得到的 response 就是重定向。
5.3 處理 upload task
上傳文件時(shí)會(huì)定期調(diào)用urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)方法,以提供上傳進(jìn)度。
URL loading system 通過(guò)以下三種方式獲取 totalBytesExpectedToSend:
- Upload body 中的
NSData的長(zhǎng)度。 - Upload task 中的 upload body 中磁盤(pán)文件的長(zhǎng)度。
- 如果為 request 顯式設(shè)置了 Content-Length,則也可以從此獲取。
另外,totalBytesSend 和 totalBytesExpectedToSend 參數(shù)也可以從URLSessionTask的countOfBytesSend和countOfBytesExpectedToSend屬性獲取。由于URLSessionTask支持ProgressReporting,還可以使用 task 的progress屬性,這樣更簡(jiǎn)潔。
5.4 處理 authentication challenge
urlSession(_:task:didReceive:completionHandler:)方法響應(yīng)服務(wù)端身份驗(yàn)證請(qǐng)求,該方法用于處理 task 級(jí)驗(yàn)證請(qǐng)求。根據(jù) authentication challenge 類型決定調(diào)用 session 級(jí)還是 task 級(jí)方法處理:
- 當(dāng)
NSURLProtectionSpace常量類型為NSURLAuthenticationMethodNTLM、NSURLAuthenticationMethodNegotiate、NSURLAuthenticationMethodClientCertificate、NSURLAuthenticationMethodServerTrust類型時(shí),URLSession實(shí)例調(diào)用會(huì)話級(jí)urlSession(_:didReceive:completionHandler:)方法響應(yīng);如果 app 沒(méi)有實(shí)現(xiàn)會(huì)話級(jí) authentication challenge 方法,URLSession實(shí)例會(huì)調(diào)用URLSessionTaskDelegate協(xié)議的urlSessoin(_:task:didReceive:completionHandler:)方法處理挑戰(zhàn)(challenge)。 - 對(duì)于非會(huì)話級(jí) challenge,
URLSession對(duì)象調(diào)用URLSessionTaskDelegate協(xié)議的urlSession(_:task:didReceive:completionHandler:)方法響應(yīng)挑戰(zhàn)。如果 app 實(shí)現(xiàn)了該方法,則必須在 task 級(jí)處理 challenge,或提供一個(gè)顯式調(diào)用會(huì)話的任務(wù)級(jí)完成處理程序。對(duì)于非會(huì)話級(jí)挑戰(zhàn),不會(huì)調(diào)用URLSessionDelegate的urlSession(_:didReceive:completionHandler:)方法。
5.5 處理 delayed waiting
在 iOS 10 中,沒(méi)有網(wǎng)絡(luò)時(shí)URLSession請(qǐng)求會(huì)立即失敗。iOS 11 中 configuration 增加了 waitsForConnectivity屬性,其值為 true 時(shí)會(huì)等有網(wǎng)絡(luò)了才發(fā)起連接。
configuration.timeoutIntervalForResource = 300
configuration.waitsForConnectivity = true
網(wǎng)絡(luò)連接可能由于多種原因不可用。例如,設(shè)備只有數(shù)據(jù)網(wǎng)絡(luò),但allowsCellularAccess屬性為NO;設(shè)備需要 VPN,但沒(méi)有可用 VPN。如果此屬性的值為true,同時(shí)連接不可用,則會(huì)話會(huì)調(diào)用urlSession(_:taskIsWaitingForConnectivity:)方法,并等待網(wǎng)絡(luò)可用。網(wǎng)絡(luò)可用后任務(wù)像往常一樣執(zhí)行。
如果waitsForConnectivity屬性為false,且網(wǎng)絡(luò)不可用,連接會(huì)立即失敗,錯(cuò)誤為 NSURLErrorNotConnectedToInternet。
waitsForConnectivity屬性只對(duì)建立連接過(guò)程有效。如果建立連接后失去網(wǎng)絡(luò),則會(huì)立即失敗,錯(cuò)誤為 NSURLErrorNetworkConnectionLost。
后臺(tái)會(huì)話會(huì)忽略waitsForConnectivity屬性,默認(rèn)等待連接。
timeoutIntervalForResource默認(rèn)為7天,這里將其設(shè)置為5分鐘。使用此配置的會(huì)話內(nèi)所有任務(wù)資源超時(shí)間隔均為300秒。timeoutIntervalForResource資源超時(shí)間隔指從請(qǐng)求發(fā)起至請(qǐng)求完成或超時(shí)。
timeoutIntervalForRequest屬性決定使用此配置的會(huì)話中所有任務(wù)的請(qǐng)求超時(shí)間隔。請(qǐng)求超時(shí)間隔指任務(wù)在放棄前等待其他數(shù)據(jù)到達(dá)的時(shí)間,單位為秒。當(dāng)新數(shù)據(jù)到達(dá)時(shí),與該值相關(guān)聯(lián)的定時(shí)器將被重置。當(dāng)計(jì)時(shí)器達(dá)到指定時(shí)間間隔而沒(méi)有接收到任何新數(shù)據(jù)時(shí)觸發(fā)超時(shí)。timeoutIntervalForRequest屬性默認(rèn)60秒。
func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) {
// Waiting for connectivity, update UI, etc.
print(task.currentRequest?.url?.absoluteString ?? "")
}
可以使用該方法更新 UI。例如,顯示呈現(xiàn)離線模式、僅蜂窩網(wǎng)絡(luò)模式。每個(gè)任務(wù)最多調(diào)用一次此方法,并且僅在建立連接不可用時(shí)調(diào)用。后臺(tái)會(huì)話不會(huì)調(diào)用該方法,因?yàn)楹笈_(tái)會(huì)話會(huì)忽略waitsForConnectivity屬性。
5.6 采集數(shù)據(jù)
收集完畢 task 的指標(biāo)(metrics)會(huì)調(diào)用urlSession(_:task:didFinishCollecting:)方法。該方法的 metrics 參數(shù)封裝了 session task 的指標(biāo)。
每個(gè)URLSessionTaskMetrics對(duì)象都包含taskInterval和redirectCount,以及任務(wù)執(zhí)行過(guò)程中進(jìn)行的每個(gè) request、response 交互。
URLSessionTaskMetrics類包含以下三個(gè)屬性:
- taskInterval:任務(wù)發(fā)起至任務(wù)完成的時(shí)間。
- redirectCount:任務(wù)執(zhí)行過(guò)程中重定向次數(shù)。
- transactionMetrics:數(shù)組內(nèi)元素為任務(wù)執(zhí)行期間每個(gè) request-response 事務(wù)度量標(biāo)準(zhǔn)。元素類型為
URLSessionTaskTransactionMetrics。
URLSessionTaskTransactionMetrics對(duì)象封裝執(zhí)行會(huì)話任務(wù)期間收集的性能指標(biāo)。每個(gè)URLSessionTaskTransactionMetrics對(duì)象包含了一個(gè) request 和 response 屬性,對(duì)應(yīng)于 task 的 request 和 response。其也包含時(shí)間指標(biāo)(temporal metrics),以fetchStartDate開(kāi)始,以responseEndDate結(jié)束,以及其他特性,例如:networkProtocolName和resourceFetchType。
下圖顯示了URL會(huì)話任務(wù)的事件序列,這些事件對(duì)應(yīng)于URLSessionTaskTransactionMetrics捕獲的時(shí)間指標(biāo)。

對(duì)于具有開(kāi)始日期和結(jié)束日期的所有指標(biāo),如果任務(wù)的某個(gè)方面未完成,則相應(yīng)指標(biāo)結(jié)束日期為 nil。在解析域名時(shí),操作超時(shí)、失敗,或客戶端在解析成功前取消了任務(wù),則可能發(fā)生這種情況。在此情況下,domainLookupEndDate屬性為 nil,其后所有指標(biāo)均為 nil。
func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) {
print("metrics: \(metrics.transactionMetrics)")
}
輸出如下:
metrics: [(Request) <NSURLRequest: 0x6000004e1730> { URL: https://audio-ssl.itunes.apple.com/itunes-assets/AudioPreview118/v4/ae/b9/f4/aeb9f43d-4bf2-3468-7163-d067ea0e38cb/mzaf_5189374696281070786.plus.aac.p.m4a }
(Response) <NSHTTPURLResponse: 0x60000066daa0> { URL: https://audio-ssl.itunes.apple.com/itunes-assets/AudioPreview118/v4/ae/b9/f4/aeb9f43d-4bf2-3468-7163-d067ea0e38cb/mzaf_5189374696281070786.plus.aac.p.m4a } { Status Code: 200, Headers {
"Accept-Ranges" = (
bytes
);
"Access-Control-Allow-Origin" = (
"*"
);
"Cache-Control" = (
"public, max-age=1296000"
);
"Content-Length" = (
1134323
);
"Content-Type" = (
"audio/x-m4a"
);
Date = (
"Sat, 21 Sep 2019 03:34:49 GMT"
);
Etag = (
"\"4960EBB73736A6F72AF3281A6A757CE1\""
);
"Last-Modified" = (
"Tue, 30 Oct 2018 20:22:39 GMT"
);
"access-control-allow-credentials" = (
false
);
"access-control-allow-headers" = (
range,
range
);
"access-control-allow-methods" = (
"HEAD, GET, PUT"
);
"access-control-max-age" = (
3000
);
cdnuuid = (
"bd23a6ea-a182-4f5c-b93c-92f2d7bd9b95-280232078"
);
"x-apple-ms-content-length" = (
1134323
);
"x-apple-request-uuid" = (
"e882fdc4-336e-4417-ac38-7c414c88d6c8",
"e882fdc4-336e-4417-ac38-7c414c88d6c8"
);
"x-cache" = (
"TCP_MISS from a23-210-215-36.deploy.akamaitechnologies.com (AkamaiGHost/9.8.2-27247474) (-)"
);
"x-cache-remote" = (
"TCP_MISS from a23-210-215-166.deploy.akamaitechnologies.com (AkamaiGHost/9.8.0.1-27187836) (-)",
"TCP_HIT from a23-210-215-166.deploy.akamaitechnologies.com (AkamaiGHost/9.8.0.1-27187836) (-)"
);
"x-icloud-availability" = (
"[DL, L, B]"
);
"x-icloud-content-length" = (
1134323
);
"x-icloud-versionid" = (
"8c4145e0-dc81-11e8-b031-248a071e6524"
);
"x-responding-server" = (
"massilia_protocol_020:620000704:qs36p01if-zteh13063901.qs.if.apple.com:8083:19R7:nocommit"
);
} }
(Fetch Start) 2019-09-21 03:34:48 +0000
(Domain Lookup Start) 2019-09-21 03:34:48 +0000
(Domain Lookup End) 2019-09-21 03:34:48 +0000
(Connect Start) 2019-09-21 03:34:48 +0000
(Secure Connection Start) 2019-09-21 03:34:49 +0000
(Secure Connection End) 2019-09-21 03:34:49 +0000
(Connect End) 2019-09-21 03:34:49 +0000
(Request Start) 2019-09-21 03:34:49 +0000
(Request End) 2019-09-21 03:34:49 +0000
(Response Start) 2019-09-21 03:34:50 +0000
(Response End) 2019-09-21 03:34:53 +0000
(Protocol Name) h2
(Proxy Connection) NO
(Reused Connection) NO
(Fetch Type) Network Load
]
可以使用上述方法查看請(qǐng)求各階段所占用的時(shí)間,優(yōu)化性能。
6. URLSessionDataDelegate
URLSessionDataDelegate協(xié)議定義了 URL session 實(shí)例處理 data task、upload task 任務(wù)級(jí)事件方法。URLSessionDataDelegate繼承自URLSessionTaskDelegate協(xié)議。
如果需要處理所有 task 類型共有的 task 級(jí)事件,還需要實(shí)現(xiàn)URLSessionTaskDelegate協(xié)議內(nèi)方法;如果需要處理 session 級(jí)事件,則需要實(shí)現(xiàn)URLSessionDelegate協(xié)議內(nèi)方法。
Data task 接收到服務(wù)器的初始回復(fù)(header)時(shí),會(huì)調(diào)用urlSession(_:dataTask:didReceive:completionHandler:)方法,該方法可選實(shí)現(xiàn),只有在接收到 response header 后需要取消任務(wù),或?qū)⑷蝿?wù)轉(zhuǎn)變?yōu)?download task 時(shí)才需要實(shí)現(xiàn)該方法。未實(shí)現(xiàn)該方法時(shí),默認(rèn)允許繼續(xù)傳輸數(shù)據(jù)。
如果需要支持相當(dāng)復(fù)雜的 multipart / x-mixed-replace 內(nèi)容類型,則需實(shí)現(xiàn)urlSession(_:dataTask:didReceive:completionHandler:)方法。在該方法內(nèi),為 completionHandler 傳入URLSession.ResponseDisposition常量。該常量有以下三個(gè)值:
-
URLSession.ResponseDisposition.allow:任務(wù)繼續(xù)作為 data task 執(zhí)行。 -
URLSession.ResponseDisposition.cancel:取消任務(wù)。 -
URLSession.ResponseDisposition.becomeDownload:調(diào)用urlSession(_:dataTask:didBecome:)方法,創(chuàng)建一個(gè) download task 取代當(dāng)前的 data task。
Data task 接收到數(shù)據(jù)時(shí)會(huì)調(diào)用urlSession(_:dataTask:didReceive:)方法。該方法可能被調(diào)用多次,每次調(diào)用提供上次調(diào)用后的數(shù)據(jù),你的 app 負(fù)責(zé)將所需數(shù)據(jù)拼接起來(lái)。
Data task 或 upload task 在接收完所有數(shù)據(jù)后會(huì)調(diào)用urlSession(_:dataTask:willCacheResponse:completionHandler:)方法,以決定是否將響應(yīng)存儲(chǔ)到緩存中。如果沒(méi)有實(shí)現(xiàn)該方法,則根據(jù)會(huì)話的 configuration 決定是否保存。該方法的主要用途在于阻止指定 URL 緩存響應(yīng),或修改緩存的 userInfo 字典。實(shí)現(xiàn)該方法后必須調(diào)用 completionHandler,傳入 proposed response 或修改后的 response 緩存數(shù)據(jù),或nil禁止緩存 response。
只有在URLProtocol協(xié)議允許緩存 response 時(shí),才會(huì)調(diào)用該方法。下面所有條件均成立時(shí)才會(huì)緩存響應(yīng):
- 請(qǐng)求是 HTTP 或 HTTPS 類型,也可以是支持緩存的自定義網(wǎng)絡(luò)協(xié)議。
- 請(qǐng)求成功,即狀態(tài)碼在200至299區(qū)間。
- response 來(lái)自服務(wù)器,而非緩存。
- 會(huì)話配置允許緩存。
-
URLRequest緩存策略允許緩存。 - 服務(wù)器響應(yīng)中與緩存相關(guān)的 header 允許緩存。
- 響應(yīng)大小足夠小,能夠進(jìn)行緩存。例如,如果提供磁盤(pán)緩存,則響應(yīng)不得大于磁盤(pán)緩存大小的5%。
7. URLSessionDownloadDelegate
URLSessionDownloadDelegate協(xié)議定義了 URL session 實(shí)例處理 download task 任務(wù)級(jí)事件方法。URLSessionDownloadDelegate繼承自URLSessionTaskDelegate協(xié)議。
8. 使用完成處理程序接收數(shù)據(jù)
獲取數(shù)據(jù)最簡(jiǎn)單的方法是創(chuàng)建 data task,并用 completion handler 處理數(shù)據(jù)。task 會(huì)將服務(wù)器的 response、data及可能的錯(cuò)誤傳遞給 completion handler。
下圖顯示了 session 與 task 關(guān)系,以及如何將結(jié)果傳遞給 completion handler。

使用dataTask(with:)方法創(chuàng)建使用完成處理程序的 data task。完成處理程序需要處理以下三件事情:
- 驗(yàn)證 error 參數(shù)是否為 nil。如果不為 nil,則傳輸時(shí)發(fā)生錯(cuò)誤。此時(shí)應(yīng)處理錯(cuò)誤并退出。
- 檢查響應(yīng)的狀態(tài)碼(status code)是否指示成功,以及 MIME 類型是否為預(yù)期值。如果不符合,處理服務(wù)器錯(cuò)誤并退出。
- 根據(jù)需要使用返回的 data。
下面的代碼使用 iTunes Search API 搜索音樂(lè):
func getSearchResult(searchTerm: String, completion: @escaping QueryResult) {
dataTask?.cancel()
if var urlComponents = URLComponents(string: "https://itunes.apple.com/search") {
urlComponents.query = "media=music&entity=song&term=\(searchTerm)"
guard let url = urlComponents.url else {
return
}
dataTask = URLSession.shared.dataTask(with: url, completionHandler: { [weak self] (data, response, error) in
defer {
self?.dataTask = nil
}
if let error = error {
print("DataTask error: " + error.localizedDescription + "\n")
return
}
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode),
let mimeType = httpResponse.mimeType,
mimeType == "text/javascript" else {
print("Status code or mime type error \n")
return
}
if let data = data {
// 處理數(shù)據(jù)
...
DispatchQueue.main.async {
// 更新UI
...
}
}
})
dataTask?.resume()
}
}
Completion handler 在 Grand Central Dispatch 其他隊(duì)列調(diào)用,與創(chuàng)建 task 隊(duì)列不同。如果需要更新 UI,需切換至主隊(duì)列。
可以在github.com/pro648/BasicDemos-iOS下載這篇文章的demo。運(yùn)行后如下:

9. 通過(guò) delegate 接收數(shù)據(jù)傳遞詳情和結(jié)果
為了更細(xì)粒度獲取任務(wù)執(zhí)行信息,在創(chuàng)建 task 時(shí)可以為 session 設(shè)置 delegate,而非使用完成處理程序。

使用上述方法,數(shù)據(jù)到達(dá)時(shí)會(huì)傳遞給URLSessionDataDelegate協(xié)議的urlSession(_:dataTask:didReceive:)方法,直到傳輸完成或失敗。傳輸過(guò)程中 delegate 也會(huì)收到其他類型事件。
下面的代碼使用URLSessionDataDelegate接收數(shù)據(jù),且只緩存 itunes.apple.com 相關(guān)域名的 response。
var receivedData: Data?
func getSearchResult(searchTerm: String) {
if var urlComponents = URLComponents(string: "https://itunes.apple.com/search") {
urlComponents.query = "media=music&entity=song&term=\(searchTerm)"
guard let url = urlComponents.url else {
return
}
receivedData = Data()
let task = session.dataTask(with: url)
task.resume()
}
}
// delegate methods
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse,
completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
guard let response = response as? HTTPURLResponse,
(200...299).contains(response.statusCode),
let mimeType = response.mimeType,
mimeType == "text/html" else {
completionHandler(.cancel)
return
}
completionHandler(.allow)
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
self.receivedData?.append(data)
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
DispatchQueue.main.async {
if let error = error {
handleClientError(error)
} else if let receivedData = self.receivedData,
// 處理接收到的數(shù)據(jù)
}
}
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse, completionHandler: @escaping (CachedURLResponse?) -> Void) {
if proposedResponse.response.url?.host == "itunes.apple.com" { // 只緩存itunes.apple.com 相關(guān)域名的 response
completionHandler(proposedResponse)
} else {
completionHandler(nil)
}
}
在實(shí)踐中切勿使用一個(gè) session 對(duì)應(yīng)一個(gè) task 的模型,應(yīng)該使用一個(gè) session 多個(gè) task。這樣有助于提高性能,更好管理內(nèi)存使用。

10. 將數(shù)據(jù)下載到文件系統(tǒng)
對(duì)于已存儲(chǔ)為文件(如圖片和文稿)的網(wǎng)絡(luò)資源,可以使用 download task 直接將這些資源提取到本地文件系統(tǒng)。
10.1 簡(jiǎn)單下載使用完成處理程序接收數(shù)據(jù)
要下載文件,從NSURLSession創(chuàng)建NSURLSessionDownloadTask對(duì)象。如果下載過(guò)程中不需要接收下載進(jìn)度,也無(wú)需處理委托回調(diào),則可以使用完成處理程序。任務(wù)下載完成或失敗時(shí)會(huì)調(diào)用完成處理程序。
完成處理程序可能收到客戶端錯(cuò)誤,用以指示本地問(wèn)題。如果沒(méi)有收到 client error,則會(huì)收到URLResponse,此時(shí)應(yīng)檢查確認(rèn)是否為成功的請(qǐng)求,且內(nèi)容類型符合預(yù)期。
如果下載成功,completion handler 會(huì)提供下載的文件在文件系統(tǒng)的臨時(shí)路徑。該存儲(chǔ)是臨時(shí)的,如果需要保存文件,則必須將文件復(fù)制、移動(dòng)到其它目錄。
如果你對(duì)文件系統(tǒng)還不了解,可以查看我的另一篇文章:使用NSFileManager管理文件系統(tǒng)
下面的代碼創(chuàng)建了一個(gè) download task,使用 completion handler 接收數(shù)據(jù)。下載成功后,將文件移動(dòng)到 cacheDirectory 目錄。
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
download(remoteURL: URL(string: "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_ts/master.m3u8")!)
}
func download(remoteURL: URL) {
let downloadTask = URLSession.shared.downloadTask(with: remoteURL) { (location, response, error) in
if let error = error {
print("error" + error.localizedDescription)
return
}
guard let httpURLResponse = response as? HTTPURLResponse,
(200...299).contains(httpURLResponse.statusCode) else {
print("server error")
return
}
guard let mimeType = httpURLResponse.mimeType,
mimeType == "audio/mpegurl" else {
print("mimeType is not audio/mpegurl")
return
}
guard let location = location else {
return
}
do {
let documentsURL = try FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
let savedURL = documentsURL.appendingPathComponent(location.lastPathComponent)
try FileManager.default.moveItem(at: location, to: savedURL)
} catch {
print("file error: \(error)")
}
}
downloadTask.resume()
}
10.2 使用 delegate 接收下載進(jìn)度更新
如果想要接收進(jìn)度更新,必須使用 delegate,實(shí)現(xiàn)URLSessionTaskDelegate、URLSessionDownloadDelegate協(xié)議內(nèi)方法。
創(chuàng)建URLSession實(shí)例,設(shè)置 delegate。下面代碼顯示了一個(gè)懶惰實(shí)例化的 downloadsSession 屬性,該屬性將 self 設(shè)置為其委托。
lazy var downloadsSession: URLSession = {
let configuration = URLSessionConfiguration.default
return URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
}()
想要開(kāi)始下載文件,使用downloadsSession創(chuàng)建URLSessionDownloadTask,調(diào)用resume()開(kāi)始下載。
func startDownload(_ track: Track) {
let download = MusicItem(track: track)
download.task = downloadsSession.downloadTask(with: track.previewURL)
download.task?.resume()
download.isDownloading = true
activeDownloads[download.track.previewURL] = download
}
10.2.1 接收進(jìn)度更新
下載開(kāi)始后,通過(guò)URLSessionDownloadDelegate中的urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesWritten:totalBytesExpectedToWrite:)方法獲取進(jìn)度更新,可以使用該函數(shù)中的回調(diào)更新下載進(jìn)度的UI。
下面的代碼演示了如何實(shí)現(xiàn)該回調(diào)方法。在該方法內(nèi)計(jì)算進(jìn)度百分比,用以更新 UI。需要注意的是,在未知的 Grand Central Dispatch 隊(duì)列中調(diào)用該方法,更新 UI 時(shí)必須切換到主隊(duì)列:
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
guard let url = downloadTask.originalRequest?.url,
let download = downloadService.activeDownloads[url] else {
return
}
download.progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
let totalSize = ByteCountFormatter.string(fromByteCount: totalBytesExpectedToWrite, countStyle: .file)
DispatchQueue.main.async {
if let trackCell = self.tableView.cellForRow(at: IndexPath(row: download.track.index,
section: 0)) as? TrackCell {
trackCell.updateDisplay(progress: download.progress, totalSize: totalSize)
}
}
}
如果下載期間需要執(zhí)行的唯一 UI 更新是
UIProgressView,則可以直接使用 task 的progress屬性,而非自行計(jì)算。progress屬性是Progress的實(shí)例。在創(chuàng)建 task 任務(wù)時(shí),將 task 的progress屬性分配給UIProgressView對(duì)象的observedProgress屬性,任務(wù)下載過(guò)程中將會(huì)自動(dòng)更新下載進(jìn)度 UI。let downloadTask = URLSession.shared.downloadTask(with: remoteURL) progressView.observedProgress = downloadTask.progress; downloadTask.resume()
10.2.2 處理下載完成或失敗
使用urlSession(_:downloadTask:didFinishDownloadingTo:)方法處理下載完成或失敗。先檢查 downloadTask 的response屬性的狀態(tài)碼,確認(rèn)下載成功。下載成功后該方法提供的 location 參數(shù)提供了文件存儲(chǔ)的位置。此位置只在回調(diào)完成前有效,這意味著必須立即讀取文件,或在回調(diào)完成前將其移動(dòng)到另一個(gè)位置。
下面代碼顯示了如何保存下載的文件:
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
guard let httpURLResponse = downloadTask.response as? HTTPURLResponse,
(200...299).contains(httpURLResponse.statusCode) else {
print("Status Code")
return
}
guard let sourceURL = downloadTask.originalRequest?.url else { return }
let download = downloadService.activeDownloads[sourceURL]
downloadService.activeDownloads[sourceURL] = nil
let destinationURL = localFilePath(for: sourceURL)
print(destinationURL)
let fileManager = FileManager.default
try? fileManager.removeItem(at: destinationURL)
do {
try fileManager.copyItem(at: location, to: destinationURL)
download?.track.downloaded = true
} catch let error {
print("Could not copy file to disk: \(error.localizedDescription)")
}
if let index = download?.track.index {
DispatchQueue.main.async { [weak self] in
self?.tableView.reloadRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
}
}
}
如果發(fā)生 client 錯(cuò)誤,會(huì)回調(diào)urlSession(_:task:didCompleteWithError:)方法。如果下載成功,則會(huì)先調(diào)用urlSession(_:downloadTask:didFinishDownloadingTo:)方法,后調(diào)用urlSession(_:task:didCompleteWithError:)方法,且此方法的 error 為 nil。
更新后運(yùn)行如下:

10.2.3 暫停下載
用戶有時(shí)需要取消正在下載的任務(wù)并在稍后恢復(fù)下載。通過(guò)支持?jǐn)帱c(diǎn)續(xù)傳,可以節(jié)省用戶時(shí)間和寬帶。
還可以使用此技術(shù)恢復(fù)由于暫時(shí)失去網(wǎng)絡(luò)連接導(dǎo)致的下載失敗。
通過(guò)調(diào)用cancelByProducingResumeData:方法取消URLSessionDownloadTask,取消完成后會(huì)調(diào)用該方法的完成處理程序。如果完成處理程序的 resumeData 參數(shù)不為 nil,則稍后使用 resumeData 恢復(fù)下載。
下面代碼演示了如何取消下載,并存儲(chǔ) resume data:
func pauseDownload(_ track: Track) {
guard let download = activeDownloads[track.previewURL],
download.isDownloading else { return }
download.task?.cancel(byProducingResumeData: { (data) in
download.resumeData = data
})
download.isDownloading = false
}
并非所有下載任務(wù)均可恢復(fù),download task 需滿足以下條件才可以恢復(fù)下載:
- 自上次請(qǐng)求后,資源未發(fā)生改變。
- Task 是 HTTP 或 HTTPS GET 請(qǐng)求。
- 服務(wù)器的響應(yīng)包含 ETag 或 Last-Modified header,也可以同時(shí)包含兩者。
- 服務(wù)器支持 byte-range 請(qǐng)求。
- 已下載的數(shù)據(jù)未被刪除。
10.2.4 下載失敗時(shí),保存已下載的數(shù)據(jù)
還可以恢復(fù)因暫時(shí)失去網(wǎng)絡(luò)而失敗的下載。例如,用戶走出 Wi-Fi 覆蓋區(qū)域。
下載失敗時(shí)會(huì)調(diào)用urlSession(_:task:didCompleteWithError:)方法。如果 error 不為 nil,讀取 error 的 userInfo 字典,查看字典NSURLSessionDownloadTaskResumeData鍵是否存在。如果 key 存在,保存其 value 用以恢復(fù)下載。如果 key 不存在,則不能恢復(fù)下載。
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
guard let error = error else {
// Handle success case.
return
}
let userInfo = (error as NSError).userInfo
if userInfo[NSLocalizedDescriptionKey] as? String == "cancelled" { // 手動(dòng)取消的下載不需要保存
return
}
if let resumeData = userInfo[NSURLSessionDownloadTaskResumeData] as? Data,
let sourceURL = task.currentRequest?.url,
let download = downloadService.activeDownloads[sourceURL]
{
download.resumeData = resumeData
download.isDownloading = false
DispatchQueue.main.async { [weak self] in
self?.tableView.reloadRows(at: [IndexPath(row: download.track.index, section: 0)], with: .automatic)
}
}
}
URLSessionTask的currentRequest表示 task 當(dāng)前的 url request,originalRequest表示 task 的初始 url request。通常兩者相同,但服務(wù)器重定向了初始請(qǐng)求時(shí)兩者會(huì)不同。另外,如果任務(wù)是通過(guò) resume data 恢復(fù)的,originalRequest為 nil,currentRequest代表當(dāng)前使用的 url request。
10.2.5 使用存儲(chǔ)的數(shù)據(jù)恢復(fù)下載
需要恢復(fù)下載時(shí),調(diào)用URLSession的downloadTask(withResumeData:)或downloadTask(withResumeData:completionHandler:)方法,傳入上一部分保存的數(shù)據(jù),并調(diào)用resume()方法,這樣即可實(shí)現(xiàn)斷點(diǎn)續(xù)傳。
func resumeDownload(_ track: Track) {
guard let download = activeDownloads[track.previewURL] else { return }
if let resumeData = download.resumeData {
download.task = downloadsSession.downloadTask(withResumeData: resumeData)
} else {
download.task = downloadsSession.downloadTask(with: download.track.previewURL)
}
download.task?.resume()
download.isDownloading = true
}
恢復(fù)下載任務(wù)后會(huì)調(diào)用urlSession(_:downloadTask:didResumeAtOffset:expectedTotalBytes:)方法。如果文件的緩存策略、最近修改日期禁止使用已下載內(nèi)容恢復(fù)下載任務(wù),則 fileOffset 參數(shù)為零;反之,fileOffset 參數(shù)為無(wú)需下載的數(shù)據(jù)大小。
在某些情況下,恢復(fù)開(kāi)始位置會(huì)在結(jié)束位置前面。
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64) {
print("fileOffset: \(fileOffset) \(expectedTotalBytes)")
}
運(yùn)行后如下:

11. 后臺(tái)下載
對(duì)于非緊急、需長(zhǎng)時(shí)間傳輸?shù)娜蝿?wù),可以創(chuàng)建后臺(tái)任務(wù)。即使應(yīng)用程序處于非活躍狀態(tài),下載也會(huì)繼續(xù)進(jìn)行,從而允許 app 恢復(fù)、重啟時(shí)訪問(wèn)下載的文件。
11.1 配置后臺(tái)會(huì)話
要在 iOS 中執(zhí)行后臺(tái)下載,需要將URLSession配置為后臺(tái)操作:
- 使用
URLSessionConfiguration對(duì)象的background(withIdentifier:)類方法創(chuàng)建配置,提供 app 內(nèi)唯一的標(biāo)志符。由于大多數(shù) app 只需要幾個(gè)后臺(tái)會(huì)話(通常為一個(gè)),因此可以使用固定字符串做為 identifier,而非動(dòng)態(tài)生成。 - 要讓系統(tǒng)在任務(wù)完成且 app 處于后臺(tái)時(shí)喚醒 app,請(qǐng)確保
sessionSendsLaunchEvents屬性設(shè)置為true。該屬性默認(rèn)為true。 - 對(duì)于非緊急任務(wù),將
isDiscretionary屬性設(shè)置為true,以便系統(tǒng)可以在最佳條件時(shí)執(zhí)行傳輸。例如,設(shè)備插入電源、連接 Wi-Fi。- 該屬性只對(duì)后臺(tái)任務(wù)有效。
- 傳輸大量數(shù)據(jù)時(shí),推薦將該屬性設(shè)置為
ture,這樣系統(tǒng)將在合適時(shí)機(jī)執(zhí)行任務(wù)。isDiscretionary屬性默認(rèn)為false。 - 只有 app 處于前臺(tái)發(fā)起的傳輸任務(wù)才會(huì)采用
isDiscretionary屬性。對(duì)于 app 處于后臺(tái)時(shí)發(fā)起的任務(wù),系統(tǒng)假定此屬性為true,并忽略你指定的任何值。
- 使用配置好的 configuration 創(chuàng)建
URLSession對(duì)象,提供 delegate 以接收后臺(tái)傳輸事件。
創(chuàng)建后臺(tái)會(huì)話:
lazy var downloadsSession: URLSession = {
let configuration = URLSessionConfiguration.background(withIdentifier: "github.com/pro648")
configuration.isDiscretionary = true
configuration.sessionSendsLaunchEvents = true
return URLSession(configuration: configuration,
delegate: self,
delegateQueue: nil)
}()
11.2 創(chuàng)建并計(jì)劃下載任務(wù)
通過(guò)downloadTask(with:)創(chuàng)建 download task,還可以設(shè)置以下屬性以幫助系統(tǒng)優(yōu)化其行為:
- 設(shè)置
earliestBeginDate屬性將下載安排在將來(lái)特定時(shí)間開(kāi)始。下載不會(huì)精確在這個(gè)時(shí)間開(kāi)始,但不會(huì)早于這個(gè)時(shí)間。 - 設(shè)置
countOfBytesClientExpectsToSend和countOfBytesClientExpectsToReceice屬性可以幫助系統(tǒng)有效地調(diào)度網(wǎng)絡(luò)活動(dòng)。屬性值是猜測(cè)預(yù)期字節(jié)數(shù)的上限,需要考慮 header 和 body。
為了方便測(cè)試,下面的代碼將下載任務(wù)計(jì)劃到 15 秒后。計(jì)劃發(fā)送 3 KB數(shù)據(jù),接收 60 MB數(shù)據(jù)。
let task = backgroundDownloadSession.downloadTask(with: remoteURL)
task.earliestBeginDate = Date().addingTimeInterval(15) // Added a delay for demonstration purposes only
task.countOfBytesClientExpectsToSend = 3 * 1024
task.countOfBytesClientExpectsToReceive = 60 * 1024 * 1024
task.resume()
設(shè)置earliestBeginDate屬性后,任務(wù)將要開(kāi)始時(shí)會(huì)調(diào)用urlSession(_:task:willBeginDelayedRequest:completionHandler:)方法。completion handler 有以下兩個(gè)參數(shù):
- DelayedRequestDisposition:要采取的措施。
- cancel:取消任務(wù)。傳遞 cancel 參數(shù)等效于 task 調(diào)用
cancel()。 - continueLoading:繼續(xù)執(zhí)行原來(lái)的 request。
- useNewRequest:執(zhí)行第二個(gè)參數(shù)提供的新 request。
- cancel:取消任務(wù)。傳遞 cancel 參數(shù)等效于 task 調(diào)用
- URLRequest:只有在 disposition 為 useNewRequest 時(shí)才會(huì)使用該參數(shù)。
只有在等待下載的過(guò)程中連接可能失效,才需要實(shí)現(xiàn)該方法。
11.3 處理 app 處于后臺(tái)狀態(tài)
不同的 app 狀態(tài)會(huì)影響 app 與后臺(tái)任務(wù)的互動(dòng)方式。在 iOS 中,app 可能處于前臺(tái)、后臺(tái)狀態(tài),也可能已被系統(tǒng)終止。
如果 app 處于后臺(tái)狀態(tài),系統(tǒng)在其他進(jìn)程執(zhí)行下載的過(guò)程中,app 可能已被系統(tǒng)掛起(suspend)。這種情況下,下載完成后系統(tǒng)會(huì)喚醒 app 并調(diào)用UIApplicationDelegate協(xié)議的application(_:handleEventsForBackgroundURLSession:completionHandler:)方法,該方法會(huì)提供創(chuàng)建該下載任務(wù)的 identifier。
該代理方法還會(huì)接收到 completion handler,將該 handler 存儲(chǔ)為 app delegate 的屬性,或?qū)崿F(xiàn)URLSessionDownloadDelegate協(xié)議類的屬性。在下面的代碼中,將 completion handler 存儲(chǔ)為BackgroundDownloadService類的屬性。
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
BackgroundDownloadService.shared.backgroundCompletionHandler = completionHandler
}
當(dāng)所有事件都已傳遞時(shí),系統(tǒng)會(huì)調(diào)用URLSessionDelegate協(xié)議的urlSessionDidFinishEvents(forBackgroundURLSession:)方法。在該方法內(nèi),獲取在上一步保存的 backgroundCompletionHandler 并執(zhí)行。
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
DispatchQueue.main.async {
self.backgroundCompletionHandler?()
self.backgroundCompletionHandler = nil
}
}
因?yàn)?code>urlSessionDidFinishEvents(forBackgroundURLSession:)方法是在輔助隊(duì)列調(diào)用,handler 是在 UIKit 獲取到的。因此,需要切換到主隊(duì)列執(zhí)行 handler。
11.4 獲取下載的文件,并移動(dòng)到永久位置
一旦喚醒的 app 調(diào)用了完成處理程序,download task 就會(huì)完成其工作并調(diào)用urlSession(_:downloadTask:didFinishDownloadingTo:)方法。此時(shí),文件已完成下載,且在方法結(jié)束前均可以訪問(wèn)該文件。這里與 app 處于前臺(tái)時(shí)下載文件一致。
11.5 App 被終止后恢復(fù)會(huì)話
如果 app 在掛起時(shí)被系統(tǒng)終止,下載完成后系統(tǒng)會(huì)在后臺(tái)重新啟動(dòng)應(yīng)用程序。作為啟動(dòng)設(shè)置的一部分,使用相同的 identifier 重新創(chuàng)建后臺(tái)會(huì)話,以允許系統(tǒng)將后臺(tái)下載任務(wù)與會(huì)話重新關(guān)聯(lián)。這樣可以確保無(wú)論 app 是由用戶還是系統(tǒng)啟動(dòng)的,后臺(tái)會(huì)話時(shí)刻準(zhǔn)備就緒。一旦 app 重新啟動(dòng),一系列事件就像 app 掛起、恢復(fù)一樣。
更新AppDelegate.swift文件中application(_:handleEventsForBackgroundURLSession:completionHandler:)方法,如下所示:
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
BackgroundDownloadService.shared.backgroundCompletionHandler = completionHandler
_ = BackgroundDownloadService.shared.backgroundDownloadSession // Make sure we have one
}
11.6 用戶手動(dòng)終止 app
如果正在進(jìn)行后臺(tái)下載,用戶手動(dòng)結(jié)束 app,則所有正在下載、已計(jì)劃的任務(wù)均會(huì)取消,且系統(tǒng)不會(huì)喚醒 app。用戶打開(kāi) app 再次執(zhí)行后臺(tái)下載時(shí),會(huì)調(diào)用urlSession(_session:task:didCompleteWithError:)方法,可以在 error 參數(shù)中提取已下載的數(shù)據(jù),根據(jù)需要決定是否恢復(fù)下載。如下所示:
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
guard let error = error else {
// Handle success case.
return
}
let userInfo = (error as NSError).userInfo
if let resumeData = userInfo[NSURLSessionDownloadTaskResumeData] as? Data,
let sourceURL = task.currentRequest?.url,
let videoItem = context.loadVideoItem(withURL: sourceURL)
{
videoItem.resumeData = resumeData
// 恢復(fù)上次手動(dòng)取消的任務(wù)
let task = backgroundDownloadSession.downloadTask(withResumeData: resumeData)
task.resume()
}
}
11.7 遵守后臺(tái)下載的限制
后臺(tái)會(huì)話由獨(dú)立于 app 的單獨(dú)進(jìn)程執(zhí)行。由于啟動(dòng) app 的進(jìn)程相當(dāng)昂貴,因此某些功能不可用,從而導(dǎo)致以下限制:
- 會(huì)話必須提供事件傳遞的 delegate。對(duì)于 download、upload,delegate 的行為與進(jìn)程內(nèi)傳輸行為相同。
- 只支持 HTTP 和 HTTPS 協(xié)議,不支持自定義協(xié)議。
- 始終允許重定向。因此,即便實(shí)現(xiàn)了
urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:)方法,也不會(huì)被調(diào)用。 - Upload task 僅支持從文件上傳。從 data 或 stream 上傳會(huì)因 app 終止而失敗。
11.8 高效的使用后臺(tái)會(huì)話
當(dāng)系統(tǒng)恢復(fù)或重啟應(yīng)用時(shí),其會(huì)使用速率限制器來(lái)防止濫用后臺(tái)會(huì)話。app 在后臺(tái)開(kāi)啟的下載任務(wù),需要經(jīng)過(guò)一個(gè)延遲才會(huì)開(kāi)始下載;每次系統(tǒng)恢復(fù)或啟動(dòng)應(yīng)用時(shí),延遲都會(huì)增加。
因此,如果 app 啟動(dòng)單個(gè)后臺(tái)下載,在下載完成后系統(tǒng)喚醒時(shí)提交新的下載,會(huì)大大增加延遲。推薦使用少量后臺(tái)會(huì)話(通常只使用一個(gè)),一次創(chuàng)建許多下載任務(wù)。這樣允許系統(tǒng)一次執(zhí)行多個(gè)下載,并在完成后恢復(fù) app。
每個(gè) task 都有自己的開(kāi)銷(overhead)。如果需要啟動(dòng)幾千個(gè)下載任務(wù),請(qǐng)將方案更改為更少次數(shù)、一次傳輸大量數(shù)據(jù)的方案。
用戶啟動(dòng) app 時(shí),延遲會(huì)重置為0;如果延遲時(shí)間已經(jīng)過(guò)去,系統(tǒng)沒(méi)有恢復(fù)、重啟 app,延遲也會(huì)重置為0。
12. Protocol Support
URLSession原生支持 data、file、ftp、http、https URL scheme,并且透明支持用戶偏好設(shè)置中的代理服務(wù)器、SOCKS 網(wǎng)關(guān)配置。
URLSession支持 HTTP/1.1 和 HTTP/2 協(xié)議,HTTP/2 需要服務(wù)器支持 Application-Layer Protocol Negotiation(ALPN)。
還可以通過(guò)繼承URLProtocol來(lái)添加自定義網(wǎng)絡(luò)協(xié)議和 URL scheme。
13. Thread Safety
URL session 自身 API 是線程安全的,可以在任意進(jìn)程創(chuàng)建 session、task。當(dāng) delegate 調(diào)用完成處理程序時(shí),其會(huì)自動(dòng)在正確的隊(duì)列執(zhí)行。
系統(tǒng)會(huì)在輔助線程調(diào)用
urlSessionDidFinishEvents(forBackgroundURLSession:)方法。在 iOS 中,實(shí)現(xiàn)該方法時(shí)需要調(diào)用application(_:handleEventsForBackgroundURLSession:completionHandler:)中的完成處理程序,而UIApplicationDelegate中的方法必須在主線程調(diào)用。
Demo名稱:URLSession
源碼地址:https://github.com/pro648/BasicDemos-iOS/tree/master/URLSession
參考資料:
本文地址:https://github.com/pro648/tips/wiki/URLSession詳解
歡迎更多指正:https://github.com/pro648/tips/wiki