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í)行其他操作?

當然,這個問題其實很簡單,解決方案也有很多。但我第一時間想到的是,目前是否存一個具有任務(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ù)下載,原因是currentRequestoriginalRequestNSKeyArchived編碼異常,iOS 10.2 及以上會修復(fù)這個問題。
    • 解決方法:獲取到resumeData后,需要對它進行修正,使用修正后的resumeData創(chuàng)建 downloadTask,再對 downloadTask 的currentRequestoriginalRequest賦值,Stack Overflow上面有具體說明。
  • 在 iOS 11.0 - iOS 11.2:
    • Bug:由于多次對 downloadTask 進行 取消 - 恢復(fù) 操作,生成的resumeData會多出一個 key 為NSURLSessionResumeByteRange的鍵值對,所以會導(dǎo)致直接下載成功(實際上沒有),下載的文件大小直接變成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ù)據(jù),所以正常來說是不應(yīng)該產(chǎn)生resumeData,在其他系統(tǒng)版本也確實沒有產(chǎn)生resumeData。如果使用這個resumeData恢復(fù)下載,會產(chǎn)生錯誤
    • 解決辦法:有兩種辦法:
      • 判斷是否存在緩存文件,由于實際上還沒有接收到任何數(shù)據(jù),自然也不會有緩存文件
      • 判斷是否已經(jīng)接收到數(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ù),這里涉及到一個重定向問題,后面會有詳細說明。

以上是目前總結(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方法,緩存文件會被刪除
  • 手動 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:)代理方法
  • 進入后臺、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)用AppDelegateapplication(_: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)用AppDelegateapplication(_: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)用AppDelegateapplication(_: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)用AppDelegateapplication(_: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 和建議。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容