本文是我在開發(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í)行其他操作?
當然,這個問題其實很簡單,解決方案也有很多。但我第一時間想到的是,目前是否存一個具有任務(wù)組概念,非常權(quán)威,非常流行、穩(wěn)定可靠,并且是用 Swift 寫的,Github 上 star 非常多的下載框架?如果存在這樣的輪子,我就打算把它作為項目里專用的下載模塊。很可惜,下載框架很多,也有很多這方面的文章和 Demo,但是像AFNetworking、SDWebImage這種著名權(quán)威,star 非常多的,真的一個都沒有,而且有一些還是用NSURLConnection實現(xiàn)的,用 Swift 寫的就更少了,這讓我有了打算自己實現(xiàn)一個的想法。
理想與現(xiàn)實
輪子這種東西,既然要自己擼,就不能隨便,而且下載框架這方面也沒權(quán)威著名的,所以一開始我打算滿足自己需求的同時,盡量能做更多的事情,爭取以后負責(zé)的項目都可以用得上。首先要滿足的就是后臺下載,眾所周知 iOS 的 App 在后臺是暫停的,那么要實現(xiàn)后臺下載,就需要按照蘋果的規(guī)定,使用URLSessionDownloadTask。
網(wǎng)上一搜就有大量的相關(guān)文章和 Demo ,然后我就開始愉快地擼代碼。結(jié)果擼到一半發(fā)現(xiàn),真正實現(xiàn)起來并且沒有網(wǎng)上的文章說得那么簡單,測試發(fā)現(xiàn)開源的輪子和 Demo 也有很多地方有 Bug,不完善,或者說沒有完整地實現(xiàn)后臺下載。于是只能靠自己繼續(xù)深入的研究,但當時確實沒有這方面研究地比較透徹文章,而時間方面也不允許,必須得盡快擼個輪子出來使用。所以最后我妥協(xié)了,我用了一個比較容易處理的辦法,改成用URLSessionDataTask實現(xiàn),雖然不是原生支持后臺下載,但我覺得總有一些邪門歪道可以實現(xiàn)的,最后我寫出了Tiercel,一個對現(xiàn)實妥協(xié)的下載框架,不過已經(jīng)滿足了我的需求。
勿忘初心
因為其實我并沒有遇到后臺下載硬性需求,所以我一直沒有尋找其他辦法去實現(xiàn),而且我覺得如果要做,就必須使用URLSessionDownloadTask,實現(xiàn)原生級別的后臺下載。隨著時間的推移,我心里一直都覺得沒有完成當初的想法是一個極大的遺憾,于是我最后下定決心,打算把 iOS 的后臺下載研究透徹。
終于,完美支持原生后臺下載的 Tiercel 2 誕生了。下面我將詳細講解后臺下載的實現(xiàn)和注意事項,希望能夠幫助有需要的人。
后臺下載
關(guān)于后臺下載,其實蘋果有提供文檔---Downloading Files in the Background,但實現(xiàn)起來要面對的問題比文檔說的要多得多。
URLSession
首先,如果需要實現(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,其實是__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的屬性,或者是全局變量,原因在后面會有詳細說明。
URLSessionDownloadTask
只有URLSessionDownloadTask才支持后臺下載
let downloadTask = urlSession.downloadTask(with: url)
downloadTask.resume()
通過Background Sessions創(chuàng)建出來的 downloadTask ,其實是__NSCFBackgroundDownloadTask
到目前為止,已經(jīng)創(chuàng)建并且開啟了支持后臺下載的任務(wù),但真正的難題,現(xiàn)在才開始
斷點續(xù)傳
蘋果的官方文檔----Pausing and Resuming Downloads
URLSessionDownloadTask 的斷點續(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ù),但實際上并沒有那么順利,resumeData就存在各種各樣的問題。
ResumeData
在 iOS 中,這個resumeData簡直就是奇葩的存在,如果你有去研究過它,你會覺得不可思議,因為這個東西一直在變,而且經(jīng)常有 Bug,似乎蘋果就是不想我們對它進行操作。
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,下載文件的唯一標識
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 進行
取消 - 恢復(fù)操作,生成的resumeData會多出一個 key 為NSURLSessionResumeByteRange的鍵值對
-
- 在 iOS 12,
resumeData編碼方式改變,需要用NSKeyedUnarchiver來解碼,結(jié)構(gòu)沒有改變 - 在 iOS 13,
NSURLSessionResumeInfoVersion = 5,結(jié)構(gòu)沒有改變
了解resumeData結(jié)構(gòu)對解決它引起的 Bug,實現(xià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后,需要對它進行修正,使用修正后的resumeData創(chuàng)建 downloadTask,再對 downloadTask 的currentRequest和originalRequest賦值,Stack Overflow上面有具體說明。
- Bug:使用系統(tǒng)生成的
- 在 iOS 11.0 - iOS 11.2:
- Bug:由于多次對 downloadTask 進行
取消 - 恢復(fù)操作,生成的resumeData會多出一個 key 為NSURLSessionResumeByteRange的鍵值對,所以會導(dǎo)致直接下載成功(實際上沒有),下載的文件大小直接變成0,iOS 11.3 及以上會修復(fù)這個問題。 - 解決方法:把 key 為
NSURLSessionResumeByteRange的鍵值對刪除。
- Bug:由于多次對 downloadTask 進行
- 在 iOS 12.0 - iOS 12.2:
- Bug:一個下載任務(wù)第一次開啟后,在還沒有接收到任何數(shù)據(jù)的時候馬上使用
cancel(byProducingResumeData:)取消任務(wù),會產(chǎn)生一個內(nèi)容為空的resumeData。由于實際上還沒有接收到任何數(shù)據(jù),所以正常來說是不應(yīng)該產(chǎn)生resumeData,在其他系統(tǒng)版本也確實沒有產(chǎn)生resumeData。如果使用這個resumeData恢復(fù)下載,會產(chǎn)生錯誤 - 解決辦法:有兩種辦法:
- 判斷是否存在緩存文件,由于實際上還沒有接收到任何數(shù)據(jù),自然也不會有緩存文件
- 判斷是否已經(jīng)接收到數(shù)據(jù)
- Bug:一個下載任務(wù)第一次開啟后,在還沒有接收到任何數(shù)據(jù)的時候馬上使用
- 在 iOS 10.3 - 最新的系統(tǒng)版本(iOS 13.3):
- Bug:從 iOS 10.3 開始,只要對 downloadTask 進行
取消 - 恢復(fù)操作,使用resumeData創(chuàng)建 downloadTask,它的originalRequest為 nil,到目前最新的系統(tǒng)版本(iOS 13.3)仍然一樣,雖然不會影響文件的下載,但會影響到下載任務(wù)的管理。 - 解決方法:使用
currentRequest匹配任務(wù),這里涉及到一個重定向問題,后面會有詳細說明。
- Bug:從 iOS 10.3 開始,只要對 downloadTask 進行
以上是目前總結(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),這也是實現(xiàn)一個下載框架最重要的環(huán)節(jié)。
下載過程中
為了測試 downloadTask 在不同情況下的表現(xiàn),花費了大量的時間和精力,具體如下:
| 操作 | 創(chuàng)建 | 運行中 | 暫停(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 |
| 進入后臺 | 自動開啟下載 | 繼續(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,真實類型是__NSCFBackgroundDownloadTask,具體表現(xiàn)跟普通的有很大的差別,根據(jù)上面的表格和蘋果官方文檔:
- 當創(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 運行中和調(diào)用
suspend方法,緩存文件會在沙盒的 caches 文件夾里 - 調(diào)用
cancel(byProducingResumeData:)方法,則緩存文件會在 Tmp 文件夾里 - 調(diào)用
cancel方法,緩存文件會被刪除
- downloadTask 運行中和調(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 會馬上停止下載,當 App 重新啟動后,創(chuàng)建對應(yīng)的
Background Sessions后,才會調(diào)用cancel(byProducingResumeData:)或者cancel方法,然后會調(diào)用urlSession(_:task:didCompleteWithError:)代理方法
- 在 iOS 8 上,手動 kill 會馬上調(diào)用
- 進入后臺、crash 或者被系統(tǒng)關(guān)閉,系統(tǒng)會有另外一個進程對下載任務(wù)進行管理,沒有開啟的任務(wù)會自動開啟,已經(jīng)開啟的會保持原來的狀態(tài)(繼續(xù)運行或者暫停),當 App 重新啟動后,創(chuàng)建對應(yīng)的
Background Sessions,可以使用session.getTasksWithCompletionHandler(_:)方法來獲取任務(wù),session 的代理方法也會繼續(xù)被調(diào)用(如果需要) - 最令人意外的是,只要沒有手動 Kill App,就算重啟手機,重啟完成后原來在運行的下載任務(wù)還是會繼續(xù)下載,實在牛逼
既然已經(jīng)總結(jié)出規(guī)律,那么處理起來就簡單了:
- 在 App 啟動的時候創(chuàng)建
Background Sessions - 使用
cancel(byProducingResumeData:)方法暫停任務(wù),保證可以恢復(fù)任務(wù)- 其實也可以使用
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保存起來 - 進入后臺、crash 或者被系統(tǒng)關(guān)閉,不影響原來任務(wù)的狀態(tài),當 App 重新啟動后,創(chuàng)建對應(yīng)的
Background Sessions后,使用session.getTasksWithCompletionHandler(_:)來獲取任務(wù)
下載完成
由于支持后臺下載,下載任務(wù)完成時,App 有可能處于不同狀態(tài),所以還要了解對應(yīng)的表現(xiàn):
- 在前臺:跟普通的 downloadTask 一樣,調(diào)用相關(guān)的 session 代理方法
- 在后臺:當
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)閉:當
Background Sessions里面所有的任務(wù)(注意是所有任務(wù),不單單是下載任務(wù))都完成后,會自動啟動 App,調(diào)用AppDelegate的application(_:didFinishLaunchingWithOptions:)方法,然后調(diào)用application(_:handleEventsForBackgroundURLSession:completionHandler:)方法,當創(chuàng)建了對應(yīng)的Background Sessions后,才會跟在前臺時一樣,調(diào)用相關(guān)的 session 代理方法,最后再調(diào)用urlSessionDidFinishEvents(forBackgroundURLSession:)方法 - crash 或者 App 被系統(tǒng)關(guān)閉,打開 App 保持前臺,當所有的任務(wù)都完成后才創(chuàng)建對應(yīng)的
Background Sessions:沒有創(chuàng)建 session 時,只會調(diào)用AppDelegate的application(_:handleEventsForBackgroundURLSession:completionHandler:)方法,當創(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é):
- 只要不在前臺,當所有任務(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)建時機,前面說過:
必須在 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中,實現(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:)
// 必須實現(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)閉,只有當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ù)進行管理,但currentRequest也有可能因為重定向而發(fā)生改變,而重定向的代理方法又不會調(diào)用,所以只能用 KVO 來觀察currentRequest,這樣就可以獲取到最新的currentRequest
最大并發(fā)數(shù)
URLSessionConfiguration里有個httpMaximumConnectionsPerHost的屬性,它的作用是控制同一個 host 同時連接的數(shù)量,蘋果的文檔顯示,默認在 macOS 里是 6,在 iOS 里是 4。單從字面上來看它的效果應(yīng)該是:如果設(shè)置為 N,則同一個 host 最多有 N 個任務(wù)并發(fā)下載,其他任務(wù)在等待,而不同 host 的任務(wù)不受這個值影響。但是實際上又有很多需要注意的地方。
- 沒有資料顯示它的最大值是多少,經(jīng)測試,設(shè)置為 1000000 都沒有問題,但是如果設(shè)置為 Int.Max,則會出問題,對于大多數(shù) URL 都是無法下載(應(yīng)該跟目標url的服務(wù)器有關(guān));如果設(shè)置為小于 1,對于大多數(shù) URL 都無法下載
- 當使用
URLSessionConfiguration.default來創(chuàng)建一個URLSession時,無論在真機還是模擬器上-
httpMaximumConnectionsPerHost設(shè)置為 10000,無論是否同一個 host,都可以有多個任務(wù)(測試過 180 多個)并發(fā)下載 -
httpMaximumConnectionsPerHost設(shè)置為 1,對于同一個 host 只能同時有一個任務(wù)在下載,不同 host可以有多個任務(wù)并發(fā)下載
-
- 當使用
URLSessionConfiguration.background(withIdentifier:)來創(chuàng)建一個支持后臺下載的URLSession- 在模擬器上
-
httpMaximumConnectionsPerHost設(shè)置為 10000,無論是否同一個 host,都可以有多個任務(wù)(測試過 180 多個)并發(fā)下載 -
httpMaximumConnectionsPerHost設(shè)置為 1,對于同一個 host 只能同時有一個任務(wù)在下載,不同 host 可以有多個任務(wù)并發(fā)下載
-
- 在真機上
-
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
-
- 在模擬器上
從以上幾點可以得出結(jié)論,由于支持后臺下載的URLSession的特性,系統(tǒng)會限制并發(fā)任務(wù)的數(shù)量,以減少資源的開銷。同時對于不同的 host,就算httpMaximumConnectionsPerHost設(shè)置為 1,也會有多個任務(wù)并發(fā)下載,所以不能使用httpMaximumConnectionsPerHost來控制下載任務(wù)的并發(fā)數(shù)。Tiercel 2 是通過判斷正在下載的任務(wù)數(shù)從而進行并發(fā)的控制。
前后臺切換
在 downloadTask 運行中,App進行前后臺切換,會導(dǎo)致urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:)方法不調(diào)用
- 在 iOS 12 - iOS 12.1,iPhone 8 以下的真機中,App 進入后臺再回到前臺,進度的代理方法不調(diào)用,當再次進入后臺的時候,有短暫的時間會調(diào)用進度的代理方法
- 在 iOS 12.1,iPhone XS 的模擬器中,多次進行前臺后臺切換,偶爾會出現(xiàn)進度的代理方法不調(diào)用,真機目測不會
- 在 iOS 11.2.2,iPhone 6 真機中,進行前臺后臺切換,會出現(xiàn)進度的代理方法不調(diào)用,多次切換則有機會恢復(fù)
以上是我測試了一些機型后發(fā)現(xiàn)的問題,沒有覆蓋全部機型,更多的情況可自行測試
解決辦法:使用通知監(jiān)聽UIApplication.didBecomeActiveNotification,延遲 0.1 秒調(diào)用suspend方法,再調(diào)用resume方法
注意事項
- 沙盒路徑:用 Xcode 運行和停止項目,可以達到 App crash 的效果,但是無論是用真機還是模擬器,每用 Xcode 運行一次,都會改變沙盒路徑,這會導(dǎo)致系統(tǒng)對 downloadTask 相關(guān)的文件操作失敗,在某些情況系統(tǒng)記錄的是上次的項目沙盒路徑,最終導(dǎo)致出現(xiàn)無法開啟任務(wù)下載、找不到文件夾等錯誤。我剛開始就是遇到這種情況,我并不知道是這個原因,所以覺得無法預(yù)測,也無法解決。各位在開發(fā)測試的時候,一定要注意。
- 真機與模擬器:由于 iOS 后臺下載的特性和注意事項實在太多,而且不同的 iOS 版本之間還存在一定的差別,所以使用模擬器進行開發(fā)和測試是一種很方便的選擇。但是有些特性在真機和模擬器上表現(xiàn)又會不一樣,例如在模擬器上下載任務(wù)的并發(fā)數(shù)是很大的,而在真機上則很?。ㄔ?iOS 12 上是 6),所以一定要在真機上進行測試或者校驗,以真機的結(jié)果為準。
- 緩存文件:前面說了恢復(fù)下載依靠的是
resumeData,其實還需要對應(yīng)的緩存文件,在resumeData里可以得到緩存文件的文件名(在 iOS 8 獲得的是緩存文件路徑),因為之前推薦使用cancel(byProducingResumeData:)方法暫停任務(wù),那么緩存文件會被移動到沙盒的 Tmp 文件夾,這個文件夾的數(shù)據(jù)在某些時候會被系統(tǒng)自動清理掉,所以為了以防萬一,最好是額外保存一份。
最后
如果大家有耐心把前面的內(nèi)容認真看完,那么恭喜你們,你們已經(jīng)了解了 iOS 后臺下載的所有特性和注意事項,同時你們也已經(jīng)明白為什么目前沒有一款完整實現(xiàn)后臺下載的開源框架,因為 Bug 和要處理的情況實在是太多。這篇文章只是我個人的一些總結(jié),可能會存在沒有發(fā)現(xiàn)問題或者細節(jié),如果有新的發(fā)現(xiàn),請給我留言。
目前 Tiercel 2 已經(jīng)發(fā)布,完美地支持后臺下載,還加入了文件校驗等功能,需要了解更多的細節(jié),可以參考代碼,歡迎各位使用,測試,提交 Bug 和建議。