iOS 原生級別后臺下載詳解

本文是我在開發(fā) Tiercel 2.0 完成后所寫的奕删,所以里面提及的是 Tiercel 2.0,目前 Tiercel 已經(jīng)開發(fā)到 3.0,文章里面的內(nèi)容依然適用,我也會繼續(xù)更新

2020/1/22 更新:iOS 13 resumeData 的結(jié)構(gòu)狐蜕、iOS 12.0 - iOS 12.2 resumeData 引起的 Bug

初衷

很久以前,我發(fā)現(xiàn)了一個將要面對的問題:

怎樣才能并發(fā)地下載一堆文件卸夕,并且全部下載完成后再執(zhí)行其他操作层释?

當(dāng)然,這個問題其實(shí)很簡單快集,解決方案也有很多贡羔。但我第一時間想到的是,目前是否存一個具有任務(wù)組概念个初,非常權(quán)威乖寒,非常流行、穩(wěn)定可靠院溺,并且是用 Swift 寫的楣嘁,Github 上 star 非常多的下載框架?如果存在這樣的輪子珍逸,我就打算把它作為項(xiàng)目里專用的下載模塊逐虚。很可惜,下載框架很多弄息,也有很多這方面的文章和 Demo痊班,但是像AFNetworkingSDWebImage這種著名權(quán)威摹量,star 非常多的涤伐,真的一個都沒有馒胆,而且有一些還是用NSURLConnection實(shí)現(xiàn)的,用 Swift 寫的就更少了凝果,這讓我有了打算自己實(shí)現(xiàn)一個的想法祝迂。

理想與現(xiàn)實(shí)

輪子這種東西,既然要自己擼器净,就不能隨便型雳,而且下載框架這方面也沒權(quán)威著名的,所以一開始我打算滿足自己需求的同時山害,盡量能做更多的事情纠俭,爭取以后負(fù)責(zé)的項(xiàng)目都可以用得上。首先要滿足的就是后臺下載浪慌,眾所周知 iOS 的 App 在后臺是暫停的冤荆,那么要實(shí)現(xiàn)后臺下載,就需要按照蘋果的規(guī)定权纤,使用URLSessionDownloadTask钓简。

網(wǎng)上一搜就有大量的相關(guān)文章和 Demo ,然后我就開始愉快地擼代碼汹想。結(jié)果擼到一半發(fā)現(xiàn)外邓,真正實(shí)現(xiàn)起來并且沒有網(wǎng)上的文章說得那么簡單,測試發(fā)現(xiàn)開源的輪子和 Demo 也有很多地方有 Bug古掏,不完善损话,或者說沒有完整地實(shí)現(xiàn)后臺下載。于是只能靠自己繼續(xù)深入的研究冗茸,但當(dāng)時確實(shí)沒有這方面研究地比較透徹文章席镀,而時間方面也不允許,必須得盡快擼個輪子出來使用夏漱。所以最后我妥協(xié)了豪诲,我用了一個比較容易處理的辦法,改成用URLSessionDataTask實(shí)現(xiàn)挂绰,雖然不是原生支持后臺下載屎篱,但我覺得總有一些邪門歪道可以實(shí)現(xiàn)的,最后我寫出了Tiercel葵蒂,一個對現(xiàn)實(shí)妥協(xié)的下載框架交播,不過已經(jīng)滿足了我的需求。

勿忘初心

因?yàn)槠鋵?shí)我并沒有遇到后臺下載硬性需求践付,所以我一直沒有尋找其他辦法去實(shí)現(xiàn)秦士,而且我覺得如果要做,就必須使用URLSessionDownloadTask永高,實(shí)現(xiàn)原生級別的后臺下載隧土。隨著時間的推移提针,我心里一直都覺得沒有完成當(dāng)初的想法是一個極大的遺憾,于是我最后下定決心曹傀,打算把 iOS 的后臺下載研究透徹辐脖。

終于,完美支持原生后臺下載的 Tiercel 2 誕生了皆愉。下面我將詳細(xì)講解后臺下載的實(shí)現(xiàn)和注意事項(xiàng)嗜价,希望能夠幫助有需要的人。

后臺下載

關(guān)于后臺下載幕庐,其實(shí)蘋果有提供文檔---Downloading Files in the Background久锥,但實(shí)現(xiàn)起來要面對的問題比文檔說的要多得多。

URLSession

首先异剥,如果需要實(shí)現(xiàn)后臺下載奴拦,就必須創(chuàng)建Background Sessions

private lazy var urlSession: URLSession = {
    let config = URLSessionConfiguration.background(withIdentifier: "com.Daniels.Tiercel")
    config.isDiscretionary = true
    config.sessionSendsLaunchEvents = true
    return URLSession(configuration: config, delegate: self, delegateQueue: nil)
}()

通過這種方式創(chuàng)建的URLSession,其實(shí)是__NSURLBackgroundSession

  • 必須使用background(withIdentifier:)方法創(chuàng)建URLSessionConfiguration届吁,其中這個identifier必須是固定的,而且為了避免跟其他 App 沖突绿鸣,建議這個identifier跟 App 的Bundle ID相關(guān)
  • 創(chuàng)建URLSession的時候疚沐,必須傳入delegate
  • 必須在 App 啟動的時候創(chuàng)建Background Sessions,即它的生命周期跟 App 幾乎一致潮模,為方便使用亮蛔,最好是作為AppDelegate的屬性,或者是全局變量擎厢,原因在后面會有詳細(xì)說明究流。

URLSessionDownloadTask

只有URLSessionDownloadTask才支持后臺下載

let downloadTask = urlSession.downloadTask(with: url)
downloadTask.resume()

通過Background Sessions創(chuàng)建出來的 downloadTask ,其實(shí)是__NSCFBackgroundDownloadTask

到目前為止动遭,已經(jīng)創(chuàng)建并且開啟了支持后臺下載的任務(wù)芬探,但真正的難題,現(xiàn)在才開始

斷點(diǎn)續(xù)傳

蘋果的官方文檔----Pausing and Resuming Downloads

URLSessionDownloadTask 的斷點(diǎn)續(xù)傳依靠的是resumeData

// 取消時保存resumeData
downloadTask.cancel { resumeDataOrNil in
    guard let resumeData = resumeDataOrNil else { return }
    self.resumeData = resumeData
}

// 或者是在session delegate 的 urlSession(_:task:didCompleteWithError:) 方法里面獲取
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
    if let error = error,
        let resumeData = (error as NSError).userInfo[NSURLSessionDownloadTaskResumeData] as? Data {
        self.resumeData = resumeData
    } 
}

// 用resumeData恢復(fù)下載
guard let resumeData = resumeData else {
    // inform the user the download can't be resumed
    return
}
let downloadTask = urlSession.downloadTask(withResumeData: resumeData)
downloadTask.resume()

正常情況下厘惦,這樣就已經(jīng)可以恢復(fù)下載任務(wù)偷仿,但實(shí)際上并沒有那么順利,resumeData就存在各種各樣的問題宵蕉。

ResumeData

在 iOS 中酝静,這個resumeData簡直就是奇葩的存在,如果你有去研究過它羡玛,你會覺得不可思議别智,因?yàn)檫@個東西一直在變,而且經(jīng)常有 Bug稼稿,似乎蘋果就是不想我們對它進(jìn)行操作薄榛。

ResumeData的結(jié)構(gòu)

在 iOS 12 之前讳窟,直接把resumeData保存為resumeData.plist到本地,可以看出里面的結(jié)構(gòu)蛇数。

  • 在 iOS 8挪钓,resumeData 的 key:
// url
NSURLSessionDownloadURL
// 已經(jīng)接受的數(shù)據(jù)大小
NSURLSessionResumeBytesReceived
// currentRequest
NSURLSessionResumeCurrentRequest
// Etag,下載文件的唯一標(biāo)識
NSURLSessionResumeEntityTag
// 已經(jīng)下載的緩存文件路徑
NSURLSessionResumeInfoLocalPath
// resumeData版本
NSURLSessionResumeInfoVersion = 1
// originalRequest
NSURLSessionResumeOriginalRequest

NSURLSessionResumeServerDownloadDate
  • 在 iOS 9 - iOS 10耳舅,改動如下:
    • NSURLSessionResumeInfoVersion = 2碌上,resumeData版本升級
    • NSURLSessionResumeInfoLocalPath改成NSURLSessionResumeInfoTempFileName,緩存文件路徑變成了緩存文件名
  • 在 iOS 11浦徊,改動如下:
    • NSURLSessionResumeInfoVersion = 4馏予,resumeData版本再次升級,應(yīng)該是直接跳過 3 了
    • 從 iOS 11 開始盔性,如果多次對 downloadTask 進(jìn)行 取消 - 恢復(fù) 操作霞丧,生成的resumeData會多出一個 key 為NSURLSessionResumeByteRange的鍵值對
  • 在 iOS 12,resumeData編碼方式改變冕香,需要用NSKeyedUnarchiver來解碼蛹尝,結(jié)構(gòu)沒有改變
  • 在 iOS 13,NSURLSessionResumeInfoVersion = 5悉尾,結(jié)構(gòu)沒有改變

了解resumeData結(jié)構(gòu)對解決它引起的 Bug突那,實(shí)現(xiàn)離線斷點(diǎn)續(xù)傳,起到關(guān)鍵作用构眯。

ResumeData的Bug

resumeData不但結(jié)構(gòu)一直變化愕难,而且也一直存在各種各樣的Bug

  • 在 iOS 10.0 - iOS 10.1:
    • Bug:使用系統(tǒng)生成的resumeData無法直接恢復(fù)下載,原因是currentRequestoriginalRequestNSKeyArchived編碼異常惫霸,iOS 10.2 及以上會修復(fù)這個問題猫缭。
    • 解決方法:獲取到resumeData后,需要對它進(jìn)行修正壹店,使用修正后的resumeData創(chuàng)建 downloadTask猜丹,再對 downloadTask 的currentRequestoriginalRequest賦值,Stack Overflow上面有具體說明硅卢。
  • 在 iOS 11.0 - iOS 11.2:
    • Bug:由于多次對 downloadTask 進(jìn)行 取消 - 恢復(fù) 操作居触,生成的resumeData會多出一個 key 為NSURLSessionResumeByteRange的鍵值對,所以會導(dǎo)致直接下載成功(實(shí)際上沒有)老赤,下載的文件大小直接變成0轮洋,iOS 11.3 及以上會修復(fù)這個問題。
    • 解決方法:把 key 為NSURLSessionResumeByteRange的鍵值對刪除抬旺。
  • 在 iOS 12.0 - iOS 12.2:
    • Bug:一個下載任務(wù)第一次開啟后弊予,在還沒有接收到任何數(shù)據(jù)的時候馬上使用cancel(byProducingResumeData:)取消任務(wù),會產(chǎn)生一個內(nèi)容為空的resumeData开财。由于實(shí)際上還沒有接收到任何數(shù)據(jù)汉柒,所以正常來說是不應(yīng)該產(chǎn)生resumeData误褪,在其他系統(tǒng)版本也確實(shí)沒有產(chǎn)生resumeData。如果使用這個resumeData恢復(fù)下載碾褂,會產(chǎn)生錯誤
    • 解決辦法:有兩種辦法:
      • 判斷是否存在緩存文件兽间,由于實(shí)際上還沒有接收到任何數(shù)據(jù),自然也不會有緩存文件
      • 判斷是否已經(jīng)接收到數(shù)據(jù)
  • 在 iOS 10.3 - 最新的系統(tǒng)版本(iOS 13.3):
    • Bug:從 iOS 10.3 開始正塌,只要對 downloadTask 進(jìn)行 取消 - 恢復(fù) 操作嘀略,使用resumeData創(chuàng)建 downloadTask,它的originalRequest為 nil乓诽,到目前最新的系統(tǒng)版本(iOS 13.3)仍然一樣帜羊,雖然不會影響文件的下載,但會影響到下載任務(wù)的管理鸠天。
    • 解決方法:使用currentRequest匹配任務(wù)讼育,這里涉及到一個重定向問題,后面會有詳細(xì)說明稠集。

以上是目前總結(jié)出的resumeData在不同的系統(tǒng)版本出現(xiàn)的改動和 Bug奶段,解決的具體代碼可以參考Tiercel

具體表現(xiàn)

支持后臺下載的 downloadTask 已經(jīng)創(chuàng)建剥纷,resumeData的問題也已經(jīng)解決忧饭,現(xiàn)在已經(jīng)可以愉快地開啟和恢復(fù)下載了。接下來要面對的是筷畦,這個 downloadTask 的具體表現(xiàn),這也是實(shí)現(xiàn)一個下載框架最重要的環(huán)節(jié)刺洒。

下載過程中

為了測試 downloadTask 在不同情況下的表現(xiàn)鳖宾,花費(fèi)了大量的時間和精力,具體如下:

操作 創(chuàng)建 運(yùn)行中 暫停(suspend) 取消(cancel(byProducingResumeData:)) 取消(cancel)
立即產(chǎn)生的效果 在 App 沙盒的 caches 文件夾里面創(chuàng)建 tmp 文件 把下載的數(shù)據(jù)寫入 caches 文件夾里面的 tmp 文件 caches 文件夾里面的 tmp 文件不會被移動 caches 文件夾里面的 tmp 文件會被移動到 Tmp 文件夾逆航,會調(diào)用 didCompleteWithError caches 文件夾里面的tmp 文件會被刪除鼎文,會調(diào)用 didCompleteWithError
進(jìn)入后臺 自動開啟下載 繼續(xù)下載 沒有發(fā)生任何事情 沒有發(fā)生任何事情 沒有發(fā)生任何事情
手動kill App 關(guān)閉的時候 caches 文件夾里面的 tmp 文件會被刪除,重新打開 App 后創(chuàng)建相同 identifier 的 session因俐,會調(diào)用 didCompleteWithError(等于調(diào)用了 cancel) 關(guān)閉的時候下載停止了拇惋,caches 文件夾里面的 tmp 文件不會被移動,重新打開 App 后創(chuàng)建相同 identifier 的 session抹剩,tmp文件會被移動到Tmp文件夾撑帖,會調(diào)用 didCompleteWithError(等于調(diào)用了 cancel(byProducingResumeData:)) 關(guān)閉的時候 caches 文件夾里面的 tmp 文件不會被移動,重新打開 App 后創(chuàng)建相同 identifier 的 session澳眷,tmp 文件會被移動到 Tmp 文件夾胡嘿,會調(diào)用 didCompleteWithError(等于調(diào)用了 cancel(byProducingResumeData:)) 沒有發(fā)生任何事情 沒有發(fā)生任何事情
crash或者被系統(tǒng)關(guān)閉 自動開啟下載,caches 文件夾里面的 tmp 文件不會被移動钳踊,重新打開 App 后衷敌,不管是否有創(chuàng)建相同 identifier 的 session勿侯,都會繼續(xù)下載(保持下載狀態(tài)) 繼續(xù)下載,caches 文件夾里面的 tmp 文件不會被移動缴罗,重新打開 App 后助琐,不管是否有創(chuàng)建相同 identifier 的 session,都會繼續(xù)下載(保持下載狀態(tài)) caches 文件夾里面的 tmp 文件不會被移動面氓,重新打開 app 后創(chuàng)建相同 identifier 的 session兵钮,不會調(diào)用 didCompleteWithError,session 里面還保存著 task侧但,此時task還是暫停狀態(tài)矢空,可以恢復(fù)下載 沒有發(fā)生任何事情 沒有發(fā)生任何事情

支持后臺下載的URLSessionDownloadTask,真實(shí)類型是__NSCFBackgroundDownloadTask禀横,具體表現(xiàn)跟普通的有很大的差別屁药,根據(jù)上面的表格和蘋果官方文檔:

  • 當(dāng)創(chuàng)建了Background Sessions,系統(tǒng)會把它的identifier記錄起來柏锄,只要 App 重新啟動后酿箭,創(chuàng)建對應(yīng)的Background Sessions,它的代理方法也會繼續(xù)被調(diào)用
  • 如果是任務(wù)被session管理趾娃,則下載中的 tmp 格式緩存文件會在沙盒的 caches 文件夾里缭嫡;如果不被session管理,且可以恢復(fù)抬闷,則緩存文件會被移動到 Tmp 文件夾里妇蛀;如果不被session管理,且不可以恢復(fù)笤成,則緩存文件會被刪除评架。即:
    • downloadTask 運(yùn)行中和調(diào)用suspend方法,緩存文件會在沙盒的 caches 文件夾里
    • 調(diào)用cancel(byProducingResumeData:)方法炕泳,則緩存文件會在 Tmp 文件夾里
    • 調(diào)用cancel方法纵诞,緩存文件會被刪除
  • 手動 Kill App 會調(diào)用了cancel(byProducingResumeData:)或者cancel方法
    • 在 iOS 8 上,手動 kill 會馬上調(diào)用cancel(byProducingResumeData:)或者cancel方法培遵,然后會調(diào)用urlSession(_:task:didCompleteWithError:)代理方法
    • 在 iOS 9 - iOS 12 上浙芙,手動 kill 會馬上停止下載,當(dāng) App 重新啟動后籽腕,創(chuàng)建對應(yīng)的Background Sessions后嗡呼,才會調(diào)用cancel(byProducingResumeData:)或者cancel方法皇耗,然后會調(diào)用urlSession(_:task:didCompleteWithError:)代理方法
  • 進(jìn)入后臺晤锥、crash 或者被系統(tǒng)關(guān)閉,系統(tǒng)會有另外一個進(jìn)程對下載任務(wù)進(jìn)行管理,沒有開啟的任務(wù)會自動開啟矾瘾,已經(jīng)開啟的會保持原來的狀態(tài)(繼續(xù)運(yùn)行或者暫停)女轿,當(dāng) App 重新啟動后,創(chuàng)建對應(yīng)的Background Sessions壕翩,可以使用session.getTasksWithCompletionHandler(_:)方法來獲取任務(wù)蛉迹,session 的代理方法也會繼續(xù)被調(diào)用(如果需要)
  • 最令人意外的是,只要沒有手動 Kill App放妈,就算重啟手機(jī)北救,重啟完成后原來在運(yùn)行的下載任務(wù)還是會繼續(xù)下載,實(shí)在牛逼

既然已經(jīng)總結(jié)出規(guī)律芜抒,那么處理起來就簡單了:

  • 在 App 啟動的時候創(chuàng)建Background Sessions
  • 使用cancel(byProducingResumeData:)方法暫停任務(wù)埂淮,保證可以恢復(fù)任務(wù)
    • 其實(shí)也可以使用suspend方法唉锌,但在 iOS 10.0 - iOS 10.1 中暫停后如果不馬上恢復(fù)任務(wù),會無法恢復(fù)任務(wù),這又是一個Bug趁矾,所以不建議
  • 手動 Kill App 會調(diào)用了cancel(byProducingResumeData:)或者cancel跨释,最后會調(diào)用urlSession(_:task:didCompleteWithError:)代理方法官地,可以在這里做集中處理缔俄,管理 downloadTask,把resumeData保存起來
  • 進(jìn)入后臺线召、crash 或者被系統(tǒng)關(guān)閉铺韧,不影響原來任務(wù)的狀態(tài),當(dāng) App 重新啟動后缓淹,創(chuàng)建對應(yīng)的Background Sessions后哈打,使用session.getTasksWithCompletionHandler(_:)來獲取任務(wù)

下載完成

由于支持后臺下載,下載任務(wù)完成時讯壶,App 有可能處于不同狀態(tài)料仗,所以還要了解對應(yīng)的表現(xiàn):

  • 在前臺:跟普通的 downloadTask 一樣,調(diào)用相關(guān)的 session 代理方法
  • 在后臺:當(dāng)Background Sessions里面所有的任務(wù)(注意是所有任務(wù)鹏溯,不單單是下載任務(wù))都完成后,會調(diào)用AppDelegateapplication(_:handleEventsForBackgroundURLSession:completionHandler:)方法淹仑,激活 App丙挽,然后跟在前臺時一樣,調(diào)用相關(guān)的 session 代理方法匀借,最后再調(diào)用urlSessionDidFinishEvents(forBackgroundURLSession:)方法
  • crash 或者 App 被系統(tǒng)關(guān)閉:當(dāng)Background Sessions里面所有的任務(wù)(注意是所有任務(wù)颜阐,不單單是下載任務(wù))都完成后,會自動啟動 App吓肋,調(diào)用AppDelegateapplication(_:didFinishLaunchingWithOptions:)方法凳怨,然后調(diào)用application(_:handleEventsForBackgroundURLSession:completionHandler:)方法,當(dāng)創(chuàng)建了對應(yīng)的Background Sessions后,才會跟在前臺時一樣肤舞,調(diào)用相關(guān)的 session 代理方法紫新,最后再調(diào)用urlSessionDidFinishEvents(forBackgroundURLSession:)方法
  • crash 或者 App 被系統(tǒng)關(guān)閉,打開 App 保持前臺李剖,當(dāng)所有的任務(wù)都完成后才創(chuàng)建對應(yīng)的Background Sessions:沒有創(chuàng)建 session 時芒率,只會調(diào)用AppDelegateapplication(_:handleEventsForBackgroundURLSession:completionHandler:)方法,當(dāng)創(chuàng)建了對應(yīng)的Background Sessions后篙顺,才會跟在前臺時一樣偶芍,調(diào)用相關(guān)的 session 代理方法,最后再調(diào)用urlSessionDidFinishEvents(forBackgroundURLSession:)方法
  • crash 或者 App 被系統(tǒng)關(guān)閉德玫,打開 App匪蟀,創(chuàng)建對應(yīng)的Background Sessions后所有任務(wù)才完成:跟在前臺的時候一樣

總結(jié):

  • 只要不在前臺,當(dāng)所有任務(wù)完成后會調(diào)用AppDelegateapplication(_:handleEventsForBackgroundURLSession:completionHandler:)方法
  • 只有創(chuàng)建了對應(yīng)Background Sessions宰僧,才會調(diào)用對應(yīng)的 session 代理方法材彪,如果不在前臺,還會調(diào)用urlSessionDidFinishEvents(forBackgroundURLSession:)

具體處理方式:

首先就是Background Sessions的創(chuàng)建時機(jī)撒桨,前面說過:

必須在 App 啟動的時候創(chuàng)建URLSession查刻,即它的生命周期跟 App 幾乎一致,為方便使用凤类,最好是作為AppDelegate的屬性穗泵,或者是全局變量。

原因:下載任務(wù)有可能在 App 處于不同狀態(tài)時完成谜疤,所以需要保證 App 啟動的時候佃延,Background Sessions也已經(jīng)創(chuàng)建,這樣才能使它的代理方法正確的調(diào)用夷磕,并且方便接下來的操作履肃。

根據(jù)下載任務(wù)完成時的表現(xiàn),結(jié)合蘋果官方文檔:

// 必須在AppDelegate中坐桩,實(shí)現(xiàn)這個方法
//
//   - identifier: 對應(yīng)Background Sessions的identifier
//   - completionHandler: 需要保存起來
func application(_ application: UIApplication,
                 handleEventsForBackgroundURLSession identifier: String,
                 completionHandler: @escaping () -> Void) {
        if identifier == urlSession.configuration.identifier ?? "" {
            // 這里用作為AppDelegate的屬性尺棋,保存completionHandler
            backgroundCompletionHandler = completionHandler
        }
}

然后要在 session 的代理方法里調(diào)用completionHandler,它的作用請看:application(_:handleEventsForBackgroundURLSession:completionHandler:)

// 必須實(shí)現(xiàn)這個方法绵跷,并且在主線程調(diào)用completionHandler
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
    guard let appDelegate = UIApplication.shared.delegate as? AppDelegate,
        let backgroundCompletionHandler = appDelegate.backgroundCompletionHandler else { return }
        
    DispatchQueue.main.async {
        // 上面保存的completionHandler
        backgroundCompletionHandler()
    }
}

至此膘螟,下載完成的情況也處理完畢

下載錯誤

支持后臺下載的 downloadTask 失敗的時候,在urlSession(_:task:didCompleteWithError:)方法里面的(error as NSError).userInfo可能會出現(xiàn)一個 key 為NSURLErrorBackgroundTaskCancelledReasonKey的鍵值對碾局,由此可以獲得只有后臺下載任務(wù)失敗時才有相關(guān)的信息荆残,具體請看:Background Task Cancellation

func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
    if let error = error {
        let backgroundTaskCancelledReason = (error as NSError).userInfo[NSURLErrorBackgroundTaskCancelledReasonKey] as? Int
    }
}

重定向

支持后臺下載的 downloadTask,由于 App 有可能處于后臺净当,或者 crash内斯,或者被系統(tǒng)關(guān)閉蕴潦,只有當(dāng)Background Sessions所有任務(wù)完成時,才會激活或者啟動俘闯,所以無法處理處理重定向的情況潭苞。

蘋果官方文檔指出:

Redirects are always followed. As a result, even if you have implemented urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:), it is not called.

意思是始終遵從重定向,并且不會調(diào)用urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:)方法备徐。

前面有提到 downloadTask 的originalRequest有可能為 nil萄传,只能用currentRequest來匹配任務(wù)進(jìn)行管理,但currentRequest也有可能因?yàn)橹囟ㄏ蚨l(fā)生改變蜜猾,而重定向的代理方法又不會調(diào)用秀菱,所以只能用 KVO 來觀察currentRequest,這樣就可以獲取到最新的currentRequest

最大并發(fā)數(shù)

URLSessionConfiguration里有個httpMaximumConnectionsPerHost的屬性蹭睡,它的作用是控制同一個 host 同時連接的數(shù)量衍菱,蘋果的文檔顯示,默認(rèn)在 macOS 里是 6肩豁,在 iOS 里是 4脊串。單從字面上來看它的效果應(yīng)該是:如果設(shè)置為 N,則同一個 host 最多有 N 個任務(wù)并發(fā)下載清钥,其他任務(wù)在等待琼锋,而不同 host 的任務(wù)不受這個值影響。但是實(shí)際上又有很多需要注意的地方祟昭。

  • 沒有資料顯示它的最大值是多少缕坎,經(jīng)測試,設(shè)置為 1000000 都沒有問題篡悟,但是如果設(shè)置為 Int.Max谜叹,則會出問題,對于大多數(shù) URL 都是無法下載(應(yīng)該跟目標(biāo)url的服務(wù)器有關(guān))搬葬;如果設(shè)置為小于 1荷腊,對于大多數(shù) URL 都無法下載
  • 當(dāng)使用URLSessionConfiguration.default來創(chuàng)建一個URLSession時,無論在真機(jī)還是模擬器上
    • httpMaximumConnectionsPerHost設(shè)置為 10000急凰,無論是否同一個 host女仰,都可以有多個任務(wù)(測試過 180 多個)并發(fā)下載
    • httpMaximumConnectionsPerHost設(shè)置為 1,對于同一個 host 只能同時有一個任務(wù)在下載抡锈,不同 host可以有多個任務(wù)并發(fā)下載
  • 當(dāng)使用URLSessionConfiguration.background(withIdentifier:)來創(chuàng)建一個支持后臺下載的URLSession
    • 在模擬器上
      • httpMaximumConnectionsPerHost設(shè)置為 10000疾忍,無論是否同一個 host,都可以有多個任務(wù)(測試過 180 多個)并發(fā)下載
      • httpMaximumConnectionsPerHost設(shè)置為 1企孩,對于同一個 host 只能同時有一個任務(wù)在下載锭碳,不同 host 可以有多個任務(wù)并發(fā)下載
    • 在真機(jī)上
      • httpMaximumConnectionsPerHost設(shè)置為 10000袁稽,無論是否同一個 host勿璃,并發(fā)下載的任務(wù)數(shù)都有限制(目前最大是 6)
      • httpMaximumConnectionsPerHost設(shè)置為 1,對于同一個 host 只能同時有一個任務(wù)在下載,不同 host 并發(fā)下載的任務(wù)數(shù)有限制(目前最大是 6)
      • 即使使用多個URLSession開啟下載补疑,可以并發(fā)下載的任務(wù)數(shù)量也不會增加
      • 以下是部分系統(tǒng)并發(fā)數(shù)的限制
        • iOS 9 iPhone SE 上是 3
        • iOS 10.3.3 iPhone 5 上是 3
        • iOS 11.2.5 iPhone 7 Plus 上是 6
        • iOS 12.1.2 iPhone 6s 上是 6
        • iOS 12.2 iPhone XS Max 上是 6

從以上幾點(diǎn)可以得出結(jié)論歧沪,由于支持后臺下載的URLSession的特性,系統(tǒng)會限制并發(fā)任務(wù)的數(shù)量莲组,以減少資源的開銷诊胞。同時對于不同的 host,就算httpMaximumConnectionsPerHost設(shè)置為 1锹杈,也會有多個任務(wù)并發(fā)下載撵孤,所以不能使用httpMaximumConnectionsPerHost來控制下載任務(wù)的并發(fā)數(shù)。Tiercel 2 是通過判斷正在下載的任務(wù)數(shù)從而進(jìn)行并發(fā)的控制竭望。

前后臺切換

在 downloadTask 運(yùn)行中邪码,App進(jìn)行前后臺切換,會導(dǎo)致urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:)方法不調(diào)用

  • 在 iOS 12 - iOS 12.1咬清,iPhone 8 以下的真機(jī)中闭专,App 進(jìn)入后臺再回到前臺,進(jìn)度的代理方法不調(diào)用旧烧,當(dāng)再次進(jìn)入后臺的時候影钉,有短暫的時間會調(diào)用進(jìn)度的代理方法
  • 在 iOS 12.1,iPhone XS 的模擬器中掘剪,多次進(jìn)行前臺后臺切換平委,偶爾會出現(xiàn)進(jìn)度的代理方法不調(diào)用,真機(jī)目測不會
  • 在 iOS 11.2.2杖小,iPhone 6 真機(jī)中肆汹,進(jìn)行前臺后臺切換,會出現(xiàn)進(jìn)度的代理方法不調(diào)用予权,多次切換則有機(jī)會恢復(fù)

以上是我測試了一些機(jī)型后發(fā)現(xiàn)的問題昂勉,沒有覆蓋全部機(jī)型,更多的情況可自行測試

解決辦法:使用通知監(jiān)聽UIApplication.didBecomeActiveNotification扫腺,延遲 0.1 秒調(diào)用suspend方法岗照,再調(diào)用resume方法

注意事項(xiàng)

  • 沙盒路徑:用 Xcode 運(yùn)行和停止項(xiàng)目,可以達(dá)到 App crash 的效果笆环,但是無論是用真機(jī)還是模擬器攒至,每用 Xcode 運(yùn)行一次,都會改變沙盒路徑躁劣,這會導(dǎo)致系統(tǒng)對 downloadTask 相關(guān)的文件操作失敗迫吐,在某些情況系統(tǒng)記錄的是上次的項(xiàng)目沙盒路徑,最終導(dǎo)致出現(xiàn)無法開啟任務(wù)下載账忘、找不到文件夾等錯誤志膀。我剛開始就是遇到這種情況熙宇,我并不知道是這個原因,所以覺得無法預(yù)測溉浙,也無法解決烫止。各位在開發(fā)測試的時候,一定要注意戳稽。
  • 真機(jī)與模擬器:由于 iOS 后臺下載的特性和注意事項(xiàng)實(shí)在太多馆蠕,而且不同的 iOS 版本之間還存在一定的差別,所以使用模擬器進(jìn)行開發(fā)和測試是一種很方便的選擇惊奇。但是有些特性在真機(jī)和模擬器上表現(xiàn)又會不一樣互躬,例如在模擬器上下載任務(wù)的并發(fā)數(shù)是很大的,而在真機(jī)上則很兴汤伞(在 iOS 12 上是 6)吨铸,所以一定要在真機(jī)上進(jìn)行測試或者校驗(yàn),以真機(jī)的結(jié)果為準(zhǔn)祖秒。
  • 緩存文件:前面說了恢復(fù)下載依靠的是resumeData诞吱,其實(shí)還需要對應(yīng)的緩存文件,在resumeData里可以得到緩存文件的文件名(在 iOS 8 獲得的是緩存文件路徑)竭缝,因?yàn)橹巴扑]使用cancel(byProducingResumeData:)方法暫停任務(wù)房维,那么緩存文件會被移動到沙盒的 Tmp 文件夾,這個文件夾的數(shù)據(jù)在某些時候會被系統(tǒng)自動清理掉抬纸,所以為了以防萬一咙俩,最好是額外保存一份。

最后

如果大家有耐心把前面的內(nèi)容認(rèn)真看完湿故,那么恭喜你們阿趁,你們已經(jīng)了解了 iOS 后臺下載的所有特性和注意事項(xiàng),同時你們也已經(jīng)明白為什么目前沒有一款完整實(shí)現(xiàn)后臺下載的開源框架坛猪,因?yàn)?Bug 和要處理的情況實(shí)在是太多脖阵。這篇文章只是我個人的一些總結(jié),可能會存在沒有發(fā)現(xiàn)問題或者細(xì)節(jié)墅茉,如果有新的發(fā)現(xiàn)命黔,請給我留言。

目前 Tiercel 2 已經(jīng)發(fā)布就斤,完美地支持后臺下載悍募,還加入了文件校驗(yàn)等功能,需要了解更多的細(xì)節(jié)洋机,可以參考代碼坠宴,歡迎各位使用,測試绷旗,提交 Bug 和建議喜鼓。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末忧设,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子颠通,更是在濱河造成了極大的恐慌,老刑警劉巖膀懈,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件顿锰,死亡現(xiàn)場離奇詭異,居然都是意外死亡启搂,警方通過查閱死者的電腦和手機(jī)硼控,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來胳赌,“玉大人牢撼,你說我怎么就攤上這事∫缮唬” “怎么了熏版?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長捍掺。 經(jīng)常有香客問我撼短,道長,這世上最難降的妖魔是什么挺勿? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任曲横,我火速辦了婚禮,結(jié)果婚禮上不瓶,老公的妹妹穿的比我還像新娘禾嫉。我一直安慰自己,他們只是感情好蚊丐,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布熙参。 她就那樣靜靜地躺著,像睡著了一般麦备。 火紅的嫁衣襯著肌膚如雪尊惰。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天泥兰,我揣著相機(jī)與錄音弄屡,去河邊找鬼。 笑死鞋诗,一個胖子當(dāng)著我的面吹牛膀捷,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播削彬,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼全庸,長吁一口氣:“原來是場噩夢啊……” “哼秀仲!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起壶笼,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤神僵,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后覆劈,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體保礼,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年责语,在試婚紗的時候發(fā)現(xiàn)自己被綠了炮障。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡坤候,死狀恐怖胁赢,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情白筹,我是刑警寧澤智末,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站徒河,受9級特大地震影響吹害,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜虚青,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一它呀、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧棒厘,春花似錦纵穿、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至何乎,卻和暖如春句惯,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背支救。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工抢野, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人各墨。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓指孤,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子恃轩,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評論 2 355