美文网首页
自定义下载器

自定义下载器

作者: AndyYaWei | 来源:发表于2020-03-30 18:07 被阅读0次

    功能:

    • 支持多任务同时下载
    • 支持断点续传

    文件结构:

    • 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 fieldRange` 设置 value.
    request.setValue(NSString(format: "bytes=%lld-", self.fileCurrentSize) as String, forHTTPHeaderField: "Range")
    
    • 在AYDownLoadManager中把URL的lastPathComponent和下载器以key/value的形式存储在NSDictionary中.

    下载路径

    下载器UML.png

    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()
            }
        }
    

    相关文章

      网友评论

          本文标题:自定义下载器

          本文链接:https://www.haomeiwen.com/subject/gezvuhtx.html