前言
在Alamofire中,还有一个断点续传的重要功能。
开始下载
首先封装一个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方法,专门用来处理下载及断点续传的功能
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!
}
暂停下载和继续下载及取消任务
暂停下载
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]
)
}
继续下载
func resume() {
self.currentDownloadRequest?.resume()
}
父类DownloadRequest里的resume方法,仍是
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]
)
}
取消下载
func cancel() {
self.currentDownloadRequest?.cancel()
}
父类DownloadRequest里的cancel方法,这里还有一步额外的操作,它保存了当前下载的resumeData,便于下次恢复下载
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方法
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,那么在第二次打开后会不会走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!
}
APP崩溃异常退出
在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处理 - 调用外界传递进来的闭包,回传相应的数据给外界