本文是我在開發(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痊班,但是像AFNetworking
、SDWebImage
這種著名權(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ù)下載,原因是currentRequest
和originalRequest
的NSKeyArchived
編碼異常惫霸,iOS 10.2 及以上會修復(fù)這個問題猫缭。 - 解決方法:獲取到
resumeData
后,需要對它進(jìn)行修正壹店,使用修正后的resumeData
創(chuàng)建 downloadTask猜丹,再對 downloadTask 的currentRequest
和originalRequest
賦值,Stack Overflow上面有具體說明硅卢。
- Bug:使用系統(tǒng)生成的
- 在 iOS 11.0 - iOS 11.2:
- Bug:由于多次對 downloadTask 進(jìn)行
取消 - 恢復(fù)
操作居触,生成的resumeData
會多出一個 key 為NSURLSessionResumeByteRange
的鍵值對,所以會導(dǎo)致直接下載成功(實(shí)際上沒有)老赤,下載的文件大小直接變成0轮洋,iOS 11.3 及以上會修復(fù)這個問題。 - 解決方法:把 key 為
NSURLSessionResumeByteRange
的鍵值對刪除抬旺。
- Bug:由于多次對 downloadTask 進(jìn)行
- 在 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ù)
- Bug:一個下載任務(wù)第一次開啟后弊予,在還沒有接收到任何數(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ì)說明稠集。
- Bug:從 iOS 10.3 開始正塌,只要對 downloadTask 進(jìn)行
以上是目前總結(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
方法纵诞,緩存文件會被刪除
- downloadTask 運(yùn)行中和調(diào)用
- 手動 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:)
代理方法
- 在 iOS 8 上,手動 kill 會馬上調(diào)用
- 進(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趁矾,所以不建議
- 其實(shí)也可以使用
- 手動 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)用AppDelegate
的application(_: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)用AppDelegate
的application(_: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)用AppDelegate
的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匪蟀,創(chuàng)建對應(yīng)的
Background Sessions
后所有任務(wù)才完成:跟在前臺的時候一樣
總結(jié):
- 只要不在前臺,當(dāng)所有任務(wù)完成后會調(diào)用
AppDelegate
的application(_: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 和建議喜鼓。