本篇講述實(shí)現(xiàn)iOS文件下載功能,包含大文件下載,后臺下載,殺死進(jìn)程,重新啟動時繼續(xù)下載,設(shè)置下載并發(fā)數(shù),監(jiān)聽網(wǎng)絡(luò)改變等。
預(yù)覽效果
附上Demo地址,如果覺得還行呢,就麻煩順手給個star
。
下載功能的實(shí)現(xiàn)
使用的網(wǎng)絡(luò)連接的類為URLSession。在iOS7時推出,至此iOS系統(tǒng)才有了后臺傳輸。在初始化URLSession前,需要先創(chuàng)建URLSessionConfiguration,可以理解為是URLSession需要的一個配置。URLSessionConfiguration有三種模式:
- default:可以使用緩存的Cache、Cookie、鑒權(quán)。
- ephemeral:僅內(nèi)存緩存,不使用緩存的Cache、Cookie、鑒權(quán)。
- background:支持后臺傳輸,需要一個identifier標(biāo)識,用來重新連接session對象。
let configuration = URLSessionConfiguration.background(withIdentifier: "CXDownloadBackgroundSessionIdentifier"
創(chuàng)建URLSession,設(shè)置配信息、代理、代理線程:
// Create `URLSession`, configure information, proxy, proxy thread.
session = URLSession(configuration: configuration, delegate: self, delegateQueue: queue)
在實(shí)現(xiàn)下載前,還需要了解一個很重要的類:URLSessionTask,無論下載多少文件,我們只需要初始化一個URLSession即可,而每個task對應(yīng)一個任務(wù),需要通過task才能實(shí)現(xiàn)下載,URLSessionTask是一個基類,有四個子類:
- URLSessionDataTask:下載時,內(nèi)容Data對象返回,需要我們不斷寫入文件。
- URLSessionUploadTask:繼承URLSessionDataTask,內(nèi)容以Data對象返回,協(xié)議方法中可以查看請求時上傳內(nèi)容的過程,支持后臺傳輸。
- URLSessionStreamTask:建立了一個TCP/IP連接,替代InputStream/OutputStream,新的API可異步讀寫,自動通過HTTP代理連接遠(yuǎn)程服務(wù)器。
- URLSessionDownloadTask:推薦使用該task實(shí)現(xiàn)文件下載,斷點(diǎn)續(xù)傳系統(tǒng)幫我們做了,資源會下載到一個臨時文件,下載完成需將文件移動至想要的路徑,系統(tǒng)會刪除臨時路勁文件,暫停時,系統(tǒng)會返回Data對象,恢復(fù)下載時用這個data創(chuàng)建task,支持后臺傳輸。
后臺下載
到這里,已經(jīng)可以通過URLSessionDataTask實(shí)現(xiàn)斷點(diǎn)續(xù)傳了,下面介紹如何實(shí)現(xiàn)后臺下載,其實(shí)非常簡單,一共三步:
- 創(chuàng)建URLSession時,需要創(chuàng)建后臺模式URLSessionConfiguration,上面已經(jīng)介紹過了。
- 在AppDelegate中實(shí)現(xiàn)下面方法,并定義變量保存completionHandler代碼塊:
// 應(yīng)用處于后臺,所有下載任務(wù)完成調(diào)用
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
CXDownloadManager.shared.setDidFinishEventsForBackgroundURLSession(completionHandler: completionHandler)
}
- 在下載類中實(shí)現(xiàn)下面URLSessionDelegate協(xié)議方法,其實(shí)就是先執(zhí)行完task的協(xié)議,保存數(shù)據(jù)、刷新界面之后再執(zhí)行在AppDelegate中保存的代碼塊:
// 應(yīng)用處于后臺,所有下載任務(wù)完成及URLSession協(xié)議調(diào)用之后調(diào)
public func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
// Execute the block, the system generates a snapshot in the background, and releases the assertion that prevents the application from being suspended.
didFinishEventsForBackgroundURLSessionHandler?()
}
程序終止,再次啟動繼續(xù)下載:
后臺下載實(shí)現(xiàn)之后,再看一下如何實(shí)現(xiàn)進(jìn)程殺死后,再次啟動時繼續(xù)下載,在應(yīng)用程序被殺掉時,系統(tǒng)會自動保存應(yīng)用下載session信息,重新啟動應(yīng)用時,如果創(chuàng)建和之前相同identifier的session,系統(tǒng)會找到對應(yīng)的session數(shù)據(jù),并響應(yīng)urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?)方法,操作如下:
public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
guard let url = task.taskDescription else {
return
}
// Process killed when downloading, callback error when restarting.
if let err = error as? NSError,
let reason = err.userInfo[NSURLErrorBackgroundTaskCancelledReasonKey] {
CXDLogger.log(message: "Reason=\(reason)", level: .info)
guard let model = CXDownloadDatabaseManager.shared.getModel(by: url)
else {
return
}
model.state = .waiting
CXDownloadDatabaseManager.shared.updateModel(model, option: .state)
return
}
let taskProcessor = downloadTaskDict[url]
taskProcessor?.processSession(task: task, didCompleteWithError: error)
}
并發(fā)數(shù)設(shè)置
下面介紹一下下載并發(fā)數(shù)的設(shè)置:URLSession本身就支持多任務(wù)同時下載,它會根據(jù)性能內(nèi)部控制同時下載的個數(shù),建議最多設(shè)置5個。一個任務(wù)對應(yīng)一個URLSessionDataTask,所以想多任務(wù)同時下載,需要創(chuàng)建多個task,可以用數(shù)組或字典保存。我們定義變量去記錄當(dāng)前下載文件個數(shù)及用戶設(shè)置的最大下載個數(shù)。
監(jiān)聽網(wǎng)絡(luò)改變
用AFN監(jiān)聽,可以點(diǎn)擊這里查看
為了增加用戶體驗(yàn),往往在設(shè)置中會給用戶一個選項(xiàng), 選擇蜂窩網(wǎng)絡(luò)下是否允許下載。URLSessionConfiguration本身就有一個屬性allowsCellularAccess,默認(rèn)為YES,允許蜂窩網(wǎng)絡(luò)下載。如果不需要用戶隨時變更這個選項(xiàng),是可以用這個屬性。但是對于正在下載的任務(wù),修改這個屬性是無效的,即我們已經(jīng)通過session創(chuàng)建了task對象,開啟了任務(wù),再試圖用。
private func setup() {
// Creates a database and a table.
_ = CXDownloadDatabaseManager.shared
currentCount = 0
let ud = UserDefaults.standard
let tmaxConcurrentCount = ud.integer(forKey: CXDownloadConfig.maxConcurrentCountKey)
maxConcurrentCount = tmaxConcurrentCount > 0 ? tmaxConcurrentCount : 1
allowsCellularAccess = ud.bool(forKey: CXDownloadConfig.allowsCellularAccessKey)
lock = NSLock()
// Single-threaded proxy queue.
queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
// Defines the background session identifier.
let configuration = URLSessionConfiguration.background(withIdentifier: "CXDownloadBackgroundSessionIdentifier")
// Allows cellular network download, the default is true, which is turned on here. We added a variable to control the user's switching choice.
configuration.allowsCellularAccess = true
// Create `URLSession`, configure information, proxy, proxy thread.
session = URLSession(configuration: configuration, delegate: self, delegateQueue: queue)
}
所以創(chuàng)建URLSessionConfiguration時把a(bǔ)llowsCellularAccess設(shè)為YES,然后定義一個變量去控制是否允許蜂窩網(wǎng)絡(luò)下載,在網(wǎng)絡(luò)狀態(tài)改變及用戶設(shè)置修改這個選項(xiàng)之后,調(diào)用暫停、開啟任務(wù)。
數(shù)據(jù)保存
用FMDB存儲數(shù)據(jù),可以點(diǎn)擊這里查看
下載速度計(jì)算
聲明兩個變量,一個記錄時間,一個記錄在特定時間內(nèi)接收到的數(shù)據(jù)大小,在接收服務(wù)器返回?cái)?shù)據(jù)的urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data)方法中,統(tǒng)計(jì)接收到數(shù)據(jù)的大小,達(dá)到時間限定時,計(jì)算速度=數(shù)據(jù)/時間,然后清空變量,為方便數(shù)據(jù)庫存儲,這里用的時間戳。
func processSession(dataTask: URLSessionDataTask, didReceive data: Data) {
let receivedBytes = dataTask.countOfBytesReceived + resumedFileSize
let allBytes = dataTask.countOfBytesExpectedToReceive + resumedFileSize
model.totalFileSize = allBytes
model.tmpFileSize = receivedBytes
let dataLength = data.count
// Calculates the size of the downloaded file within the speed time.
model.intervalFileSize += Int64(dataLength)
let intervals = CXDToolbox.getIntervalsWithTimestamp(model.lastSpeedTime)
if intervals > 1 {
// Calculates speed
model.speed = model.intervalFileSize / intervals
model.lastSpeedTime = CXDToolbox.getTimestampWithDate(Date())
}
let progress = Float(receivedBytes) / Float(allBytes)
//CXDLogger.log(message: "progress: \(progress)", level: .info)
model.progress = progress
// Update the specified model in database.
CXDownloadDatabaseManager.shared.updateModel(model, option: .progressData)
postProgressNotification()
// Reset it.
model.intervalFileSize = 0
}
點(diǎn)贊+關(guān)注,第一時間獲取技術(shù)干貨和最新知識點(diǎn),謝謝你的支持!
最后祝大家生活愉快~