美文网首页
自定义下载器

自定义下载器

作者: 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()
        }
    }

相关文章

  • 自定义下载器

    功能: 支持多任务同时下载 支持断点续传 文件结构: AYFileTool AYDownLoader AYDown...

  • FileReader 读取文件流下载异常问题

    场景 服务器返回文件流,用于下载。前端需要自定义下载文件名,因此需要使用javascript处理文件流blob。 ...

  • Centos7-Anaconda安装与使用

    1. 下载 这里对于不同地区的服务器我们采用不同的地址进行下载, 进入到自定义的安装目录中运行wget下载命令. ...

  • Linux下自定义安装JDK

    自定义安装JDK 1、下载jdk安装包的几种方式 (1)通过浏览器把安装包下载到安装目录下(如:/usr/loca...

  • java 类加载器

    自定义类加载器 自定义类加载器运行

  • iOS自定义日期选择器 隐藏超出范围的时间

    自定义日期选择器 总有一款是你需要的???? github下载地址:https://github.com/zhuz...

  • 微信暂存

    “Proxyee Down是一款开源的免费 HTTP 高速下载器,底层使用netty开发,支持自定义 HTTP 请...

  • iOS中xib与storyboard各种加载

    xib 加载自定义View xib 加载自定义控制器 storyboard 加载自定义控制器 xib 加载自定义cell

  • iOS按钮点击水波纹效果

    1.首先自定义一个按钮 ZFButton 创建方法 定时器调用方法 最后绘制水纹 最后献上demo 下载地址

  • Macaca_使用

    下载示例包 #执行测试用例 macaca-simple-reportor 是自定义报告器的一个示例,可以作为参考。...

网友评论

      本文标题:自定义下载器

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