iOS断点下载

23 阅读4分钟

断点下载:是指在下载一个较大文件过程中因网络中断等主动暂停下载,当重启任下载任务时,能够从上次停止的位置继续下载,而不用重新下载。

知识点:

1.URLSession及其任务管理

URLSessionDownloadTask:是实现断点下载的核心类,专门用于下载文件到临时位置,并原生支持断点续传:

相关代码:

let configuration = URLSessionConfiguration.default

var downloadTask : URLSessionDownloadTask?

let fileURL = URL(string: "http://vjs.zencdn.net/v/oceans.mp4")

任务下载

let session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)

downloadTask = session.downloadTask(with: URLRequest(url: fileURL!))
downloadTask?.resume()

继续下载

let session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)

downloadTask = session.downloadTask(withResumeData: data)
downloadTask?.resume()

取消下载

downloadTask?.cancel(byProducingResumeData: { [weak self] resumeData in
   self?.downloadTask = nil
   // 其他操作
}

2.数据持久化

下载的过程本身是不处理相关数据的存储的,需要我们自己来实现。数据持久化的方式很多但支持断点下载功能的多半都是比较大型的文件。因此选择沙盒(SandBox)来存储下载的文件是十分合适的。

获取文件目录:一般都是把文件存储到documentDirectoryuserDomainMask目录

let fileManager = FileManager.default

let documentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!

创建写入文件路径:这里表示把文件写入MyVideos文件,文件名为:oceans.mp4

let folderName = documentDaiectory.appendingPathComponent("MyVideos")

let videoURL = folderName.appendingPathComponent("oceans.mp4")

在上一步获取文件目录已经指定了一个根目录这个会沙盒系统的根目录下再创建一个MyVideos文件

// 创建需要的文件目录
do {
   try fileManager.createDirectory(at: folderName, withIntermediateDirectories: true, attributes: nil)
   // 写入文件
} catch {
   print("创建目录失败:\(error)")
}

写入文件

do {
    try data.write(to: videoURL)
    print("写入成功")
} catch {
    print("写入失败:\(error)")
}

下次继续下载时要去做一个判断,查看是否已经存储之前下载的内容,返回TRUE则是进行继续下载,返回FALSE则是重新开始下载

let fileManager = FileManager.default

guard let documentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else {
   return false
}

let documentsFileURL = documentDirectory.appendingPathComponent("MyVideos/oceans.mp4")

if fileManager.fileExists(atPath: documentsFileURL.path) {
  do {
      // 存在 
      // 同时获取当一已下载文件的Data
      self.currentDownloadData = try Data(contentsOf: documentsFileURL)
      return true
  } catch {
      return false
  }
} else {
  return true
}

在对返回的状态做相应的处理

if isFileExist() == true {
   // 继续下载
} else {
  // 重新下载
}

3.URLSessionDownloadDelegate

除了相关下载存储操作外还要实现 URLSessionDownloadDelegate 相关代理方法

下载完成:通过URLSessionDownloadTask下载完成的文件并不会存储到指定的文件夹,而是存储在sandbox的tmp目录下的临时文件夹内。该文件夹内的数据随时都会被系统清理,因此要在适当的时候把文件转移到我们需要的文件下。

这里我们把文件存储到""MyVideos"文件下并使用"oceans.mp4"为文件名

func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
   let fileManager = FileManager.default
   let documentDirectory = FileManager.default.urls(for:.documentDirectory, in: .userDomainMask).first!
   let fileURL = documentDirectory.appendingPathComponent("MyVideos/oceans.mp4")

   do {
      if isFileExist() == true {
         // 还是对文件是否存在做一个判断并做一个删除处理,因为沙盒系统本身不会自动覆盖同名文件的处理
         try fileManager.removeItem(at: fileURL)
      }
      
      // 移动到指定目录
      try fileManager.moveItem(at: location, to: fileURL)
   } catch {
      print("删除文件出错:\(error)")
   }
}

下载过程中方法:可以从该方法获取到下载的进度

func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
   self.currentProgressValue = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
}

核心代码

import UIKit
import Foundation

typealias DownloadProgressBlock = (_ progreee : Float) -> ()
typealias DownloadFileBlock = (_ fileURL : URL) -> ()

class WZGVideoDownloader : NSObject {
    static var shard = WZGVideoDownloader()

    var progressBlock : DownloadProgressBlock?
  
    var fileBlock : DownloadFileBlock?

    let configuration = URLSessionConfiguration.default
    var downloadTask : URLSessionDownloadTask?
    let fileURL = URL(string: "http://vjs.zencdn.net/v/oceans.mp4")
    
    // 存储已下载data
    var currentDownloadData : Data?

    // 当前文件大小
    var currentProgressValue : Float = 0.0
    
    func startDownload(_ fileSize : Data) {
        let fileManager = FileManager.default
        let documentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
        let documentFileURL = documentDirectory.appendingPathComponent("MyVideos/oceans.mp4")

        let session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)

        // 判断是继续下载还是重新下载
        if isFileExist() == true {
            if let data = self.currentDownloadData {
                if data == fileSize {
                    self.progressBlock?(1)
                    self.fileBlock?(documentFileURL)
                    return
                }
                self.progressBlock?(self.currentProgressValue)

                // 继续下载
                print("继续下载")
                downloadTask = session.downloadTask(withResumeData: data)
                downloadTask?.resume()
            }

        } else {
            // 重新下载
            print("重新下载")
            downloadTask = session.downloadTask(with: URLRequest(url: fileURL!))
            downloadTask?.resume()
        }
    }
    
    func stopDownload() {
        downloadTask?.cancel(byProducingResumeData: { [weak self] resumeData in
            guard let resumeData = resumeData else {
                return
            }
            self?.writeSandBox(resumeData)
            self?.downloadTask = nil
        })
    }
    
    // 判断是否有下载的文件

    func isFileExist() -> Bool {
        let fileManager = FileManager.default
        guard let documentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else {
            return false
        }

        let documentsFileURL = documentDirectory.appendingPathComponent("MyVideos/oceans.mp4")

        if fileManager.fileExists(atPath: documentsFileURL.path) {
            do {
                self.currentDownloadData = try Data(contentsOf: documentsFileURL)
                print("currentDownloadData:\(currentDownloadData)")
                return true
            } catch {
                return false
            }
        } else {
            return false
        }
    }
    
    // 写入sandbox

    func writeSandBox(_ data : Data) {
        let fileManager = FileManager.default
        let documentDaiectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!

        //创建目录几写入文件名
        let folderName = documentDaiectory.appendingPathComponent("MyVideos")

        //设置写入文件名称
        let videoURL = folderName.appendingPathComponent("oceans.mp4")

        // 创建目录
        do {
            try fileManager.createDirectory(at: folderName, withIntermediateDirectories: true, attributes: nil)
            // 写入文件
            do {
                try data.write(to: videoURL)
                print("写入成功")
            } catch {
                print("写入失败:\(error)")
            }
        } catch {
            print("创建目录失败:\(error)")
        }
    }
}

extension WZGVideoDownloader : URLSessionDownloadDelegate {
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        let fileManager = FileManager.default
        let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
        let fileURL = documentDirectory.appendingPathComponent("MyVideos/oceans.mp4")
        do {
            if isFileExist() == true {
                // 文件存在则删除
                try fileManager.removeItem(at: fileURL)
            }
            // 下载完会保存在temp零食文具目录 转移至需要的目录
            try fileManager.moveItem(at: location, to: fileURL)
            self.fileBlock?(fileURL)
        } catch {
            print("删除文件出错:\(error)")
        }
    }

    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
        self.currentProgressValue = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
        DispatchQueue.main.async {
            self.progressBlock?(self.currentProgressValue)
        }
    }
}