功能:
- 支持多任务同时下载
- 支持断点续传
文件结构:
- AYFileTool
- AYDownLoader
- AYDownLoadManager
技术要点:
- 文件存储:把url和下载的文件大小以NSDictionary的形式write在沙盒里.下载的数据通过文件流写入沙盒,具体使用如下:
let stream = OutputStream(toFileAtPath: fullPath, append: true)
self.stream = stream
self.stream?.open()
let bytes = [UInt8](data)
self.stream?.write(UnsafePointer<UInt8>(bytes), maxLength: bytes.count)
self.stream?.close()
self.stream = nil
- 使用信号量同步请求结果:使用信号量当信号量为0时,semaphore.wait()堵塞当前线程,请求结果回来后,semaphore.signal()信号量加1,继续顺序执行代码.
- 断点续传:使用NSMutableURLRequest的
setValue:forHTTPHeaderField:给 HTTP header field
Range` 设置 value.
request.setValue(NSString(format: "bytes=%lld-", self.fileCurrentSize) as String, forHTTPHeaderField: "Range")
- 在AYDownLoadManager中把URL的lastPathComponent和下载器以key/value的形式存储在NSDictionary中.
AYFileTool
获取文件大小
static func getFileSize(filePath: String) -> Int64 {
if !FileManager.default.fileExists(atPath: filePath) {
return 0
}
var fileDict = [FileAttributeKey: Any]()
do {
fileDict = try FileManager.default.attributesOfItem(atPath: filePath)
} catch(let error) {
print("error========\(error.localizedDescription)")
return 0
}
return fileDict[FileAttributeKey.size] as? Int64 ?? 0
}
删除文件
static func removeFile(filePath: String) {
do {
try FileManager.default.removeItem(atPath: filePath)
} catch (let error) {
print("error========\(error.localizedDescription)")
}
}
转换文件大小
static func calculateFileSizeInUnit(contentLength: Double) -> CGFloat {
if contentLength >= pow(1024, 3) {
return CGFloat(contentLength) / (pow(1024, 3))
} else if contentLength > pow(1024, 2) {
return CGFloat(contentLength) / (pow(1024, 2))
} else if contentLength > 1024 {
return CGFloat(contentLength) / 1024
} else {
return CGFloat(contentLength)
}
}
转换单位
static func calculateUnit(contentLength: Double) -> String {
if contentLength >= pow(1024, 3) {
return "GB"
} else if contentLength >= pow(1024, 2) {
return "MB"
} else if contentLength >= 1024 {
return "KB"
} else {
return "Bytes"
}
}
AYDownLoader
下载方法
func downLoad(url: URL?, progressBlock: ((_ progress: CGFloat) -> Void)?, successBlock: ((_ downLoadPath : String) -> Void)?, failBlock: (() -> Void)?) {
downLoadURL = url
self.progressBlock = progressBlock
self.successBlock = successBlock
self.failBlock = failBlock
if self.isDowning {
print("正在下载....")
return
}
// 1. 获取需要下载的文件头信息
let result = getRemoteFileMessage()
if !result {
print("下载出错,请重新尝试")
self.failBlock?()
isDowning = false
return
}
// 2. 根据需要下载的文件头信息,验证本地信息
// 2.1 如果本地文件存在
// 进行一下验证:
// 文件大小 == 服务器文件大小;文件已经存在,不需要处理
// 文件大小 > 服务器文件大小;删除本地文件,重新下载
// 文件大小 < 服务器文件大小;根据本地缓存,继续断点下载
// 2.2 如果文件不存在,则直接下载
let isRequireDownLoad = checkLocalFile()
if isRequireDownLoad {
print("根据文件缓存大小, 执行下载操作")
startDownLoad()
} else {
print("文件已经存在---\(String(describing: self.fileFullPath))")
self.successBlock?(self.fileFullPath!)
}
}
开始下载
func startDownLoad() {
isDowning = true
let session = URLSession(configuration: URLSessionConfiguration.default, delegate: self, delegateQueue: OperationQueue.main)
guard let url = self.downLoadURL else { return }
let request = NSMutableURLRequest(url: url)
request.setValue(NSString(format: "bytes=%lld-", self.fileCurrentSize) as String, forHTTPHeaderField: "Range")
self.downLoadTask = session.dataTask(with: request as URLRequest)
self.downLoadTask?.resume()
}
暂停下载
func pauseDownLoad() {
isDowning = false
self.downLoadTask?.suspend()
}
继续下载
func resumeDownLoad() {
print("继续")
isDowning = true
if self.downLoadTask != nil {
self.downLoadTask!.resume()
} else {
downLoad(url: self.downLoadURL, progressBlock: self.progressBlock, successBlock: self.successBlock, failBlock: self.failBlock)
}
}
取消下载
func cancelDownLoad() {
print("取消")
isDowning = false
self.downLoadTask?.cancel()
print("-----\(String(describing: self.downLoadTask?.state))")
self.downLoadTask = nil
try? FileManager.default.removeItem(atPath: self.fileFullPath ?? "")
}
获取下载文件的信息
func getRemoteFileMessage() -> Bool {
// 对信息进行本地缓存, 方便下次使用
let headerMsgPath = (kLocalPath as NSString).appendingPathComponent(kHeaderFilePath)
guard let fileName = self.downLoadURL?.lastPathComponent else {
return false
}
var dic = NSMutableDictionary(contentsOfFile: headerMsgPath)
if dic == nil {
dic = NSMutableDictionary()
}
let containsKey = dic?.allKeys.contains {
return $0 as? String == fileName
}
if let isContains = containsKey, isContains == true {
self.fileTotalSize = (dic?[fileName] as? Int64) ?? 0
self.fileFullPath = (kLocalPath as NSString).appendingPathComponent(fileName)
return true
}
guard let url = self.downLoadURL else {
return false
}
var isCanGet = false
var request = URLRequest(url: url, cachePolicy: NSURLRequest.CachePolicy.reloadIgnoringLocalCacheData, timeoutInterval: 30.0)
request.httpMethod = "HEAD"
// 使用信号量-同步请求
let semaphore = DispatchSemaphore(value: 0)
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
if error == nil {
self.fileTotalSize = Int64((response?.expectedContentLength) ?? 0)
if let suggestedFilename = response?.suggestedFilename {
self.fileFullPath = (kLocalPath as NSString).appendingPathComponent(suggestedFilename)
}
dic?.setValue(self.fileTotalSize, forKey: fileName)
dic?.write(toFile: headerMsgPath, atomically: true)
isCanGet = true
} else {
isCanGet = false
}
semaphore.signal()
}
task.resume()
semaphore.wait()
return isCanGet
}
获取文件大小
static func cacheFileSize(url: URL) -> Int64 {
let path = (kLocalPath as NSString).appendingPathComponent(url.lastPathComponent)
return AYFileTool.getFileSize(filePath: path)
}
删除文件
static func removeCacheFile(url: URL){
let path = (kLocalPath as NSString).appendingPathComponent(url.lastPathComponent)
AYFileTool.removeFile(filePath: path)
}
检测文件是否需要下载
func checkLocalFile() -> Bool {
guard let fullPath = self.fileFullPath else {
print("路径有问题")
return false
}
self.fileCurrentSize = AYFileTool.getFileSize(filePath: fullPath)
if self.fileCurrentSize > self.fileTotalSize {
// 删除文件,并重新下载
AYFileTool.removeFile(filePath: fullPath)
return true
}
if self.fileCurrentSize < self.fileTotalSize {
return true
}
return false
}
AYDownLoadManager
根据key获取下载器
func loader(url: URL?) -> AYDownLoader? {
guard let uri = url else {
return nil
}
return self.downLoadDic[uri.lastPathComponent]
}
下载方法
func downLoad(url: URL, progressBlock: @escaping ((_ progress: CGFloat) -> Void), successBlock: @escaping ((_ fileFullPath: String) -> Void), failBlock: @escaping (() -> Void)) {
/*
var downLoader = self.loader(url: url)
if let loader = downLoader {
loader.resumeDownLoad()
} else {
downLoader = AYDownLoader()
self.downLoadDic[url.lastPathComponent] = downLoader!
downLoader?.downLoad(url: url, progressBlock: { (progress) in
progressBlock(progress)
}, successBlock: { [weak self] (downLoadPath :String) in
guard let weakSelf = self else { return }
successBlock(downLoadPath)
// 移除对象
weakSelf.downLoadDic.removeValue(forKey: (downLoadPath as NSString).lastPathComponent)
}, failBlock: {
failBlock()
})*/
var downLoader = self.loader(url: url)
if downLoader == nil {
downLoader = AYDownLoader()
}
self.downLoadDic[url.lastPathComponent] = downLoader!
downLoader?.downLoad(url: url, progressBlock: { (progress) in
progressBlock(progress)
}, successBlock: { [weak self] (downLoadPath :String) in
guard let weakSelf = self else { return }
successBlock(downLoadPath)
// 移除对象
weakSelf.downLoadDic.removeValue(forKey: (downLoadPath as NSString).lastPathComponent)
}, failBlock: {
failBlock()
})
}
暂停下载
func pauseDownLoad(url: URL){
let downloader = loader(url: url)
downloader?.pauseDownLoad()
}
继续下载
func resumeDownLoad(url: URL){
let downloader = loader(url: url)
downloader?.resumeDownLoad()
}
取消下载
func cancelDownLoad(url: URL) {
let downLoader = downLoadDic[url.lastPathComponent]
if let loader = downLoader {
loader.cancelDownLoad()
} else {
AYDownLoader.removeCacheFile(url: url)
}
}
取消所有下载
func cancelAllTasks() {
self.downLoadDic.forEach { (key, value) in
(value as AYDownLoader).cancelDownLoad()
downLoadDic.removeValue(forKey: key)
}
}
暂停下载
func pauseAllTasks(){
self.downLoadDic.forEach { (key, value) in
(value as AYDownLoader).pauseDownLoad()
}
}
网友评论