提到下载就会想到软件下载、游戏下载,这些都是大文件下载,下载时间长,如果这时候进入后台或app推出了怎么办?今天来说说下载那些事。
(一)后台下载
既然做下载功能,那就直接做成后台下载吧:
DowloadManager.shared.sessionManager
.download(url) { (url, response) -> (destinationURL: URL, options: DownloadRequest.DownloadOptions) in
let documentUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
let fileUrl = documentUrl?.appendingPathComponent(response.suggestedFilename!)
return (fileUrl!, [.removePreviousFile, .createIntermediateDirectories])//存储路径, [删除以前的文件, 创建中间目录]
}
.response { (response) in
print("完成")
}
.downloadProgress { (progress) in
print("下载进度")
}
class DowloadManager: NSObject {
static let shared = DowloadManager()
let sessionManager: SessionManager = {
let configuration = URLSessionConfiguration.background(withIdentifier: "fireDowload")
configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders
configuration.sharedContainerIdentifier = "group.fireDowload"//需要用到后台Session,则必需给此属性设置一个有效的ID值
return SessionManager(configuration: configuration)
}()
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
//后台下载完成回调通知系统
DowloadManager.shared.sessionManager.backgroundCompletionHandler = completionHandler
}
- 后台下载注意点:
Alamofire
的SessionManager
需要做成单例,否则报错:Error Domain=NSURLErrorDomain Code=-999 "cancelled"
;系统的 API 就不用单例- 记得
AppDelegate
的completionHandler
的调用
- 源码分析

(二)断点续传
下载文件往往比较大,时间长了,会遇见很多种情况,比如进入后台、暂停后重启app再下载、强杀app、app闪退:
DowloadManager.shared.download(url)
class DowloadManager: NSObject {
static let shared = DowloadManager()
private var request: DownloadRequest?
private var resumeData: Data?
private var documentUrl: URL {
return FileManager.default.urls(for: .documentDirectory, in: FileManager.SearchPathDomainMask.userDomainMask).first!
}
//初始化
let sessionManager: SessionManager = {
/* 1. 为了避免跟其他App冲突,建议这个identifier 跟应用程序的 Bundle ID相关,保证唯一;
2. URLSession必须传入delegate;
3. 最好在App启动的时候创建 */
let configuration = URLSessionConfiguration.background(withIdentifier: "com.fireDowload")
configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders
configuration.sharedContainerIdentifier = "group.fireDowload"//需要用到后台Session,则必需给此属性设置一个有效的ID值
let manager = SessionManager(configuration: configuration)
//因为session使用同一个Identifier,所以:
/* 一. 下载时强杀app,重启会来到taskDidComplete */
manager.delegate.taskDidComplete = { (seesion,task, error) in
if let error = error, let resumeData = (error as NSError).userInfo[NSURLSessionDownloadTaskResumeData] as? Data {
/* tmp是临时文件,包含resumeData和其他数据;
断点续传就是拿到tmp的resumeData便可继续下载;
所以需要保存resumeData,不然下次正常重启app没有resumeData */
DowloadManager.shared.resumeData = resumeData
DowloadManager.shared.saveResumeData(resumeData)
}
}
/* 二. 下载时app闪退,系统会在后台继续下载到完成,
完成后再重启app会来到downloadTaskDidFinishDownloadingToURL */
manager.delegate.downloadTaskDidFinishDownloadingToURL = { (session, downloadTask, url) in
//移动下载好的文件
DowloadManager.shared.moveFile(downloadTask: downloadTask, url: url)
}
/* 三. app闪退后,在系统后台进程还没下载完成时,
重启app会来到downloadTaskDidWriteData,
此时可以拿到task和进度 */
manager.delegate.downloadTaskDidWriteData = { (session, downloadTask, bytesWritten, totalBytesWritten, totalBytesExpectedToWrite) in
print(totalBytesWritten/totalBytesExpectedToWrite)//进度
}
return manager
}()
//下载
func download(url: String) ->DownloadRequest {
if let resumeData = getLastResumeData() {
self.request = sessionManager.download(resumingWith: resumeData)
}else {
self.request = sessionManager.download(url) { [weak self](url, response) -> (destinationURL: URL, options: DownloadRequest.DownloadOptions) in
let fileUrl = self?.documentUrl.appendingPathComponent(response.suggestedFilename!)
return (fileUrl!, [.removePreviousFile, .createIntermediateDirectories])//存储路径, [删除以前的文件, 创建中间目录]
}
}
return self.request!.response { (response) in
print("完成")
}.downloadProgress { (progress) in
print("下载进度")
}
}
//获取data
func getLastResumeData() -> Data? {
if self.resumeData == nil {
let fileUrl = DowloadManager.shared.documentUrl.appendingPathComponent("resumeData.tmp")
self.resumeData = try? Data.init(contentsOf: fileUrl)
}
return self.resumeData
}
//保存data
func saveResumeData(_: Data?) {
let fileUrl = DowloadManager.shared.documentUrl.appendingPathComponent("resumeData.tmp")
try? resumeData?.write(to: fileUrl)
}
//移动文件
func moveFile(downloadTask: URLSessionDownloadTask, url: URL) {
guard let response = downloadTask.response as? HTTPURLResponse else { return }
let fileUrl = DowloadManager.shared.documentUrl.appendingPathComponent(response.suggestedFilename!)
do {
if FileManager.default.fileExists(atPath: fileUrl.path) {
try FileManager.default.removeItem(at: fileUrl)
}
let directory = fileUrl.deletingLastPathComponent()
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
//移动下载好的文件
try FileManager.default.moveItem(at: url, to: fileUrl)
} catch {
print(error)
}
}
//暂停
func suspend() {
self.request?.suspend()
//保存
saveResumeData(self.request?.resumeData)
}
//启动
func resume() {
self.request?.resume()
}
//取消
func cancel() {
self.request?.cancel()
}
}
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
//后台下载完成回调通知系统
DowloadManager.shared.sessionManager.backgroundCompletionHandler = completionHandler
}
- 源码分析



网友评论