URLSession詳解

這篇文章介紹了 URL Loading System 相關(guān)知識(shí)脆丁,涉及以下內(nèi)容:

  • URLSession類型。
  • URLSessionTask類型
  • URLSessionDelegateURLSessionTaskDelegate睦刃、URLSessionDataDelegate辣卒、URLSessionDownloadDelegate四種協(xié)議。
  • 使用URLSessionDataTask請(qǐng)求數(shù)據(jù)隙姿。
  • 使用URLSessionDownloadTask下載、暫停厂捞、恢復(fù)下載視頻输玷。支持?jǐn)帱c(diǎn)續(xù)傳、后臺(tái)下載靡馁,下載完成后自動(dòng)保存到相冊(cè)欲鹏。
URLSessionPreview.png

URL 加載系統(tǒng)(URL Loading System)使用標(biāo)準(zhǔn)協(xié)議(如 https)或自定義協(xié)議提供對(duì) URL 標(biāo)識(shí)資源進(jì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茵宪。例如最冰,瀏覽器為正常瀏覽和無痕模式使用單獨(dú)的 session,無痕瀏覽不會(huì)保存數(shù)據(jù)到磁盤眉厨。下圖顯示了具有這些配置的兩個(gè) session 如何創(chuàng)建多個(gè) task:

URLSessionCreateTasks.png

每個(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)證雕沉、重定向通知等集乔。

通過URLSession API,可以創(chuàng)建一個(gè)或多個(gè) session坡椒,每個(gè) session 協(xié)調(diào)一組數(shù)據(jù)傳輸任務(wù)扰路。例如,如果你在開發(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ù)深碱、是否允許通過蜂窩網(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ì)象重寫猿挚。默認(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)沒有配置對(duì)象,一般用于基本請(qǐng)求姚淆。單例會(huì)話不能像自己創(chuàng)建的會(huì)話一樣進(jìn)行配置孕蝉,但如果需求非常有限,其是一個(gè)很好的起點(diǎn)腌逢。通過調(diào)用shared類方法獲取單例會(huì)話昔驱。

2.1 default

Default session 和 shared session 類似,但允許自定義配置上忍,且可以通過 delegate 獲取增量數(shù)據(jù)骤肛;默認(rèn)使用基于磁盤的持久緩存(下載文件除外),并將憑據(jù)(credential)保存到用戶 keychain窍蓝,還會(huì)將 cookie 保存到共享的 cookie store腋颠。通過調(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,而非寫入磁盤面睛。只有在告訴會(huì)話將數(shù)據(jù)寫入文件時(shí)絮蒿,ephemeral 類型會(huì)話才會(huì)將數(shù)據(jù)寫入磁盤。通過調(diào)用NSURLSessionConfiguration類的ephemeral方法創(chuàng)建 ephemeral session 配置叁鉴。

也可以自定義 default configuration 以獲得與 ephemeral configuration 相同的功能土涝,但直接使用 ephemeral configuration 更為方便。

使用 ephemeral session 的主要優(yōu)點(diǎn)在于保護(hù)隱私幌墓。通過將敏感數(shù)據(jù)保存到 RAM 取代寫入磁盤但壮,避免數(shù)據(jù)被攔截、它用常侣。因此蜡饵,ephemeral session 非常適合瀏覽器無痕瀏覽模式。

由于 ephemeral session 不會(huì)將緩存數(shù)據(jù)保存到磁盤胳施,緩存大小會(huì)受限于可用 RAM溯祸。這一限制決定了可緩存數(shù)據(jù)大小,也會(huì)影響 app 性能。用戶退出并重新啟動(dòng) app焦辅,所有緩存會(huì)被清空鸟召。

App 使會(huì)話無效時(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í)行莺匠。

通過調(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,用來獲取 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)打開 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 將資源直接下載到磁盤上的文件舞吭。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。如果沒有特別用途衷旅,不需要維護(hù)對(duì)任務(wù)的引用捐腿。

Task 還有progresscountOfBytesReceived柿顶、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 開始或結(jié)束,data task腻要、download task 定期進(jìn)度更新复罐。

urlSession(_:didBecomeInvalidWithError:)方法用以通知 URL session 該 session 已失效。如果通過調(diào)用finishTasksAndInvalidate()方法使會(huì)話無效雄家,會(huì)話會(huì)在最后一個(gè) task 完成或失敗后調(diào)用該方法效诅;如果通過調(diào)用invalidateAndCancel()方法使會(huì)話無效,會(huì)話立即調(diào)用該方法。

urlSession(_:didReceive:completionHandler:)方法響應(yīng)來自遠(yuǎn)程服務(wù)器的會(huì)話級(jí)身份驗(yàn)證請(qǐng)求乱投。遇到以下兩種情況時(shí)會(huì)調(diào)用該方法:

  • 遠(yuǎn)程服務(wù)器請(qǐng)求客戶端證書咽笼,或 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í)現(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ò)誤峦筒。例如無法解析主機(jī)名究西、無法連接主機(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 通過以下三種方式獲取 totalBytesExpectedToSend:

  • Upload body 中的NSData的長(zhǎng)度襟企。
  • Upload task 中的 upload body 中磁盤文件的長(zhǎng)度。
  • 如果為 request 顯式設(shè)置了 Content-Length闸溃,則也可以從此獲取整吆。

另外拱撵,totalBytesSend 和 totalBytesExpectedToSend 參數(shù)也可以從URLSessionTaskcountOfBytesSendcountOfBytesExpectedToSend屬性獲取辉川。由于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汇跨、NSURLAuthenticationMethodClientCertificateNSURLAuthenticationMethodServerTrust類型時(shí)妆距,URLSession實(shí)例調(diào)用會(huì)話級(jí)urlSession(_:didReceive:completionHandler:)方法響應(yīng)穷遂;如果 app 沒有實(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)用URLSessionDelegateurlSession(_:didReceive:completionHandler:)方法掠剑。

5.5 處理 delayed waiting

在 iOS 10 中,沒有網(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澜公,但沒有可用 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ì)建立連接過程有效。如果建立連接后失去網(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í)間間隔而沒有接收到任何新數(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ì)象都包含taskIntervalredirectCount,以及任務(wù)執(zhí)行過程中進(jìn)行的每個(gè) request撩扒、response 交互似扔。

URLSessionTaskMetrics類包含以下三個(gè)屬性:

  • taskInterval:任務(wù)發(fā)起至任務(wù)完成的時(shí)間。
  • redirectCount:任務(wù)執(zhí)行過程中重定向次數(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開始缝裤,以responseEndDate結(jié)束屏轰,以及其他特性,例如:networkProtocolNameresourceFetchType憋飞。

下圖顯示了URL會(huì)話任務(wù)的事件序列霎苗,這些事件對(duì)應(yīng)于URLSessionTaskTransactionMetrics捕獲的時(shí)間指標(biāo)。

URLSessionTaskTransactionMetrics.png

對(duì)于具有開始日期和結(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ù)拼接起來。

Data task 或 upload task 在接收完所有數(shù)據(jù)后會(huì)調(diào)用urlSession(_:dataTask:willCacheResponse:completionHandler:)方法奸例,以決定是否將響應(yīng)存儲(chǔ)到緩存中彬犯。如果沒有實(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 來自服務(wù)器罚缕,而非緩存艇纺。
  • 會(huì)話配置允許緩存。
  • URLRequest緩存策略允許緩存怕磨。
  • 服務(wù)器響應(yīng)中與緩存相關(guān)的 header 允許緩存喂饥。
  • 響應(yīng)大小足夠小,能夠進(jìn)行緩存肠鲫。例如员帮,如果提供磁盤緩存,則響應(yīng)不得大于磁盤緩存大小的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。

URLSessionCompletionHandler.png

使用dataTask(with:)方法創(chuàng)建使用完成處理程序的 data task听盖。完成處理程序需要處理以下三件事情:

  1. 驗(yàn)證 error 參數(shù)是否為 nil胀溺。如果不為 nil裂七,則傳輸時(shí)發(fā)生錯(cuò)誤。此時(shí)應(yīng)處理錯(cuò)誤并退出仓坞。
  2. 檢查響應(yīng)的狀態(tài)碼(status code)是否指示成功背零,以及 MIME 類型是否為預(yù)期值。如果不符合无埃,處理服務(wù)器錯(cuò)誤并退出徙瓶。
  3. 根據(jù)需要使用返回的 data。

下面的代碼使用 iTunes Search API 搜索音樂:

    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)行后如下:

URLSessionShared.gif

9. 通過 delegate 接收數(shù)據(jù)傳遞詳情和結(jié)果

為了更細(xì)粒度獲取任務(wù)執(zhí)行信息,在創(chuàng)建 task 時(shí)可以為 session 設(shè)置 delegate蒲稳,而非使用完成處理程序。

URLSessionDelegate.png

使用上述方法伍派,數(shù)據(jù)到達(dá)時(shí)會(huì)傳遞給URLSessionDataDelegate協(xié)議的urlSession(_:dataTask:didReceive:)方法江耀,直到傳輸完成或失敗。傳輸過程中 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)存使用盐杂。

URLSessionOneSessionMoreTask.png

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ì)象剔应。如果下載過程中不需要接收下載進(jìn)度睡腿,也無需處理委托回調(diào),則可以使用完成處理程序峻贮。任務(wù)下載完成或失敗時(shí)會(huì)調(diào)用完成處理程序席怪。

完成處理程序可能收到客戶端錯(cuò)誤,用以指示本地問題纤控。如果沒有收到 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)URLSessionTaskDelegateURLSessionDownloadDelegate協(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)
    }()

想要開始下載文件,使用downloadsSession創(chuàng)建URLSessionDownloadTask愚铡,調(diào)用resume()開始下載蛉签。

    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)度更新

下載開始后,通過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ù)下載過程中將會(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)行如下:

URLSessionDownload.gif
10.2.3 暫停下載

用戶有時(shí)需要取消正在下載的任務(wù)并在稍后恢復(fù)下載。通過支持?jǐn)帱c(diǎn)續(xù)傳蕾盯,可以節(jié)省用戶時(shí)間和寬帶幕屹。

還可以使用此技術(shù)恢復(fù)由于暫時(shí)失去網(wǎng)絡(luò)連接導(dǎo)致的下載失敗。

通過調(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)
            }
        }
    }

URLSessionTaskcurrentRequest表示 task 當(dāng)前的 url request伟骨,originalRequest表示 task 的初始 url request。通常兩者相同燃异,但服務(wù)器重定向了初始請(qǐng)求時(shí)兩者會(huì)不同携狭。另外,如果任務(wù)是通過 resume data 恢復(fù)的回俐,originalRequest為 nil逛腿,currentRequest代表當(dāng)前使用的 url request。

10.2.5 使用存儲(chǔ)的數(shù)據(jù)恢復(fù)下載

需要恢復(fù)下載時(shí)鲫剿,調(diào)用URLSessiondownloadTask(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ù)為無需下載的數(shù)據(jù)大小汽摹。

在某些情況下,恢復(fù)開始位置會(huì)在結(jié)束位置前面苦锨。

    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64) {
        print("fileOffset: \(fileOffset) \(expectedTotalBytes)")
    }

運(yùn)行后如下:

URLSessionResumeDownload.gif

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í)訪問下載的文件氏仗。

11.1 配置后臺(tái)會(huì)話

要在 iOS 中執(zhí)行后臺(tái)下載,需要將URLSession配置為后臺(tái)操作:

  1. 使用URLSessionConfiguration對(duì)象的background(withIdentifier:)類方法創(chuàng)建配置夺鲜,提供 app 內(nèi)唯一的標(biāo)志符皆尔。由于大多數(shù) app 只需要幾個(gè)后臺(tái)會(huì)話(通常為一個(gè)),因此可以使用固定字符串做為 identifier币励,而非動(dòng)態(tài)生成慷蠕。
  2. 要讓系統(tǒng)在任務(wù)完成且 app 處于后臺(tái)時(shí)喚醒 app,請(qǐng)確保sessionSendsLaunchEvents屬性設(shè)置為true榄审。該屬性默認(rèn)為true砌们。
  3. 對(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,并忽略你指定的任何值镶蹋。
  4. 使用配置好的 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ù)

通過downloadTask(with:)創(chuàng)建 download task贺归,還可以設(shè)置以下屬性以幫助系統(tǒng)優(yōu)化其行為:

  • 設(shè)置earliestBeginDate屬性將下載安排在將來特定時(shí)間開始淆两。下載不會(huì)精確在這個(gè)時(shí)間開始,但不會(huì)早于這個(gè)時(shí)間拂酣。
  • 設(shè)置countOfBytesClientExpectsToSendcountOfBytesClientExpectsToReceice屬性可以幫助系統(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ù)將要開始時(shí)會(huì)調(diào)用urlSession(_:task:willBeginDelayedRequest:completionHandler:)方法性含。completion handler 有以下兩個(gè)參數(shù):

  • DelayedRequestDisposition:要采取的措施。
    • cancel:取消任務(wù)鸳惯。傳遞 cancel 參數(shù)等效于 task 調(diào)用cancel()商蕴。
    • continueLoading:繼續(xù)執(zhí)行原來的 request。
    • useNewRequest:執(zhí)行第二個(gè)參數(shù)提供的新 request芝发。
  • URLRequest:只有在 disposition 為 useNewRequest 時(shí)才會(huì)使用該參數(shù)绪商。

只有在等待下載的過程中連接可能失效,才需要實(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í)行下載的過程中决采,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é)束前均可以訪問該文件。這里與 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)。這樣可以確保無論 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卦方。用戶打開 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ì)使用速率限制器來防止濫用后臺(tái)會(huì)話瓣距。app 在后臺(tái)開啟的下載任務(wù)黔帕,需要經(jīng)過一個(gè)延遲才會(huì)開始下載;每次系統(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 都有自己的開銷(overhead)虾攻。如果需要啟動(dòng)幾千個(gè)下載任務(wù),請(qǐng)將方案更改為更少次數(shù)更鲁、一次傳輸大量數(shù)據(jù)的方案霎箍。

用戶啟動(dòng) app 時(shí),延遲會(huì)重置為0澡为;如果延遲時(shí)間已經(jīng)過去漂坏,系統(tǒng)沒有恢復(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)拴孤。

還可以通過繼承URLProtocol來添加自定義網(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

參考資料:

  1. URL Loading System

  2. URLSession Tutorial: Getting Started

  3. Programming-iOS-Book-Examples Background Download

  4. KEEPING THINGS GOING WHEN THE USER LEAVES

  5. URLSession Waiting For Connectivity

本文地址:https://github.com/pro648/tips/wiki/URLSession詳解
歡迎更多指正:https://github.com/pro648/tips/wiki

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末猎提,一起剝皮案震驚了整個(gè)濱河市获三,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌锨苏,老刑警劉巖疙教,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異伞租,居然都是意外死亡贞谓,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門葵诈,熙熙樓的掌柜王于貴愁眉苦臉地迎上來裸弦,“玉大人祟同,你說我怎么就攤上這事±砀恚” “怎么了晕城?”我有些...
    開封第一講書人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)窖贤。 經(jīng)常有香客問我砖顷,道長(zhǎng),這世上最難降的妖魔是什么赃梧? 我笑而不...
    開封第一講書人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任滤蝠,我火速辦了婚禮,結(jié)果婚禮上槽奕,老公的妹妹穿的比我還像新娘几睛。我一直安慰自己,他們只是感情好粤攒,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開白布所森。 她就那樣靜靜地躺著,像睡著了一般夯接。 火紅的嫁衣襯著肌膚如雪焕济。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評(píng)論 1 301
  • 那天盔几,我揣著相機(jī)與錄音晴弃,去河邊找鬼。 笑死逊拍,一個(gè)胖子當(dāng)著我的面吹牛上鞠,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播芯丧,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼芍阎,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了缨恒?” 一聲冷哼從身側(cè)響起谴咸,我...
    開封第一講書人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎骗露,沒想到半個(gè)月后岭佳,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡萧锉,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年珊随,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片柿隙。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡玫恳,死狀恐怖辨赐,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情京办,我是刑警寧澤,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布帆焕,位于F島的核電站惭婿,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏叶雹。R本人自食惡果不足惜财饥,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望折晦。 院中可真熱鬧钥星,春花似錦、人聲如沸满着。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽风喇。三九已至宁改,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間魂莫,已是汗流浹背还蹲。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留耙考,地道東北人谜喊。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像倦始,于是被迫代替她去往敵國(guó)和親斗遏。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容