在Alamofire
中,還有一個斷點續傳的重要功能。swift
首先封裝一個DLDowloadManager
,便於處理緩存
class DLDowloadManager: NSObject {
static let shared = DLDowloadManager()
var currentDownloadRequest: DownloadRequest?
var resumeData: Data?
var downloadTasks: Array<URLSessionDownloadTask>?
var filePath: URL{
return FileManager.default.urls(for: .documentDirectory, in: FileManager.SearchPathDomainMask.userDomainMask).first!.appendingPathComponent("com.download.denglei.cn")
}
//單例方便獲取
let manager: SessionManager = {
let configuration = URLSessionConfiguration.background(withIdentifier: "com.denglei.AlamofireDowload")
configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders
configuration.sharedContainerIdentifier = "group.com.denglei.AlamofireDowload"
let manager = SessionManager(configuration: configuration)
manager.startRequestsImmediately = true
}
}
複製代碼
這裏封裝一個download
方法,專門用來處理下載及斷點續傳的功能bash
func download(_ url: URLConvertible) -> DownloadRequest {
//沒有緩存直接下載
currentDownloadRequest = DLDowloadManager.shared.manager.download(url) { [weak self](url, reponse) -> (destinationURL: URL, options: DownloadRequest.DownloadOptions) in
let fileUrl = self?.filePath.appendingPathComponent(reponse.suggestedFilename!)
return (fileUrl!,[.removePreviousFile, .createIntermediateDirectories] )
return currentDownloadRequest!
}
複製代碼
暫停下載session
func suspend() {
self.currentDownloadRequest?.suspend()
}
複製代碼
父類DownloadRequest
裏的suspend
方法,經過調用task.suspend
來暫停下載任務閉包
open func suspend() {
guard let task = task else { return }
task.suspend()
NotificationCenter.default.post(
name: Notification.Name.Task.DidSuspend,
object: self,
userInfo: [Notification.Key.Task: task]
)
}
複製代碼
繼續下載app
func resume() {
self.currentDownloadRequest?.resume()
}
複製代碼
父類DownloadRequest
裏的resume
方法,還是ide
open func resume() {
guard let task = task else { delegate.queue.isSuspended = false ; return }
if startTime == nil { startTime = CFAbsoluteTimeGetCurrent() }
task.resume()
NotificationCenter.default.post(
name: Notification.Name.Task.DidResume,
object: self,
userInfo: [Notification.Key.Task: task]
)
}
複製代碼
取消下載post
func cancel() {
self.currentDownloadRequest?.cancel()
}
複製代碼
父類DownloadRequest
裏的cancel
方法,這裏還有一步額外的操做,它保存了當前下載的resumeData
,便於下次恢復下載ui
open override func cancel() {
downloadDelegate.downloadTask.cancel { self.downloadDelegate.resumeData = $0 }
NotificationCenter.default.post(
name: Notification.Name.Task.DidCancel,
object: self,
userInfo: [Notification.Key.Task: task as Any]
)
}
複製代碼
那麼咱們在取消任務後,再次開始下載,須要判斷task
裏的resumeData
是否存在,若是存在則繼續上次下載,修改後的download
方法url
func download(_ url: URLConvertible) -> DownloadRequest {
//取消任務後繼續下載
if let resumeData = DLDowloadManager.shared.currentDownloadRequest?.resumeData {
currentDownloadRequest = DLDowloadManager.shared.manager.download(resumingWith: resumeData)
}else{
//沒有緩存直接下載
currentDownloadRequest = DLDowloadManager.shared.manager.download(url) { [weak self](url, reponse) -> (destinationURL: URL, options: DownloadRequest.DownloadOptions) in
let fileUrl = self?.filePath.appendingPathComponent(reponse.suggestedFilename!)
return (fileUrl!,[.removePreviousFile, .createIntermediateDirectories] )
}
}
return currentDownloadRequest!
}
複製代碼
前面寫的是正常使用時的斷點續傳功能,還有用戶主動殺死APP 和 APP出現崩潰異常退出的狀況須要處理。
首先猜測,若是用戶以前主動殺死APP,那麼在第二次打開後會不會走APP裏的某些代理方法呢?
在以前對request
的解析過程當中,知道全部系統的代理回調都會來到SessionDelegate
裏的代理裏,因而在Download
有關的代理方法裏都打上斷點。
再次用Xcode
運行APP後,發現didCompleteWithError
裏的斷點被執行了,說明會運行到這裏
在上面的代碼裏,strongSelf.taskDidComplete?(session, task, error)
,咱們發現會先執行當前的代理taskDidComplete
在外界DownloadManager
裏監聽一下這個taskDidComplete
,把裏面的resumeData
保存起來便可
// 用戶kill 進來
manager.delegate.taskDidComplete = { (seesion,task, error) in
if let error = error {
print("taskDidComplete的error狀況: \(error)")
if let resumeData = (error as NSError).userInfo[NSURLSessionDownloadTaskResumeData] as? Data {
// resumeData 存儲
DLDowloadManager.shared.resumeData = resumeData
print("來了")
}
}else{
print("taskDidComplete的task狀況: \(task)")
}
}
複製代碼
download
方法,多加一種判斷來處理這種狀況func download(_ url: URLConvertible) -> DownloadRequest {
//處理用戶主動殺死APP的狀況
if self.resumeData != nil {
currentDownloadRequest = DLDowloadManager.shared.manager.download(resumingWith: self.resumeData!)
}else{
//取消任務後繼續下載
if let resumeData = DLDowloadManager.shared.currentDownloadRequest?.resumeData {
currentDownloadRequest = DLDowloadManager.shared.manager.download(resumingWith: resumeData)
}else{
//沒有緩存直接下載
currentDownloadRequest = DLDowloadManager.shared.manager.download(url) { [weak self](url, reponse) -> (destinationURL: URL, options: DownloadRequest.DownloadOptions) in
let fileUrl = self?.filePath.appendingPathComponent(reponse.suggestedFilename!)
return (fileUrl!,[.removePreviousFile, .createIntermediateDirectories] )
}
}
}
return currentDownloadRequest!
}
複製代碼
在VC中主動製造一個崩潰的異常
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let array = [1]
print(array[2])
}
複製代碼
而後再次打開APP,等待一會發現居然再次來到了上面說的回調方法didCompleteWithError
,發現error
是空的,手動去找文件的目錄發現文件也是正常下載完成的
說明在APP上次崩潰退出後,再次進來會開啓一個後臺下載任務,繼續下載上次崩潰的任務,直到下載完成
因此咱們在taskDidComplete
也能監聽到下載完成, 會和用戶主動殺死APP最後來到同一個回調,能夠一塊兒監聽
// 用戶kill 進來
manager.delegate.taskDidComplete = { (seesion,task, error) in
if let error = error {
print("taskDidComplete的error狀況: \(error)")
if let resumeData = (error as NSError).userInfo[NSURLSessionDownloadTaskResumeData] as? Data {
// resumeData 存儲
DLDowloadManager.shared.resumeData = resumeData
print("來了")
}
}else{
print("taskDidComplete的task狀況: \(task)")
}
}
複製代碼
downloadTaskDidFinishDownloadingToURL
裏也能監聽到下載完成的回調內容,在這裏能夠根據不一樣的下載完成的url作一些處理manager.delegate.downloadTaskDidFinishDownloadingToURL = { (session, downloadTask, url) in
}
複製代碼
還有一個問題,咱們在這個後臺下載任務裏如何監聽下載進度,並更新到UI上呢?
因爲會走下載完成的代理方法,那麼確定也會走正在下載的代理方法didWriteData
在這個代理方法裏能夠拿到本次下載的字節bytesWritten
,一共已經下載的字節totalBytesWritten
和一共須要下載的字節totalBytesExpectedToWrite
因此咱們在DLDowloadManager
裏可以經過這個代理方法獲取到下載的進度,利用這裏的數據而後再在外界更新UI便可
manager.delegate.downloadTaskDidWriteData = {(session, downloadTask, bytesWritten, totalBytesWritten, totalBytesExpectedToWrite) in
}
複製代碼
在實現斷點續傳功能的整個過程當中,再一次感覺到了Alamofire
的結構和流程
SessionDelegate
DownloadTask
任務SessionDelegate
接收系統URLSession
的下載回調DownloadTaskDelegate
處理