iOS断点下载

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

知识点:

1.URLSession及其任务管理

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

相关代码:

csharp 复制代码
let configuration = URLSessionConfiguration.default

var downloadTask : URLSessionDownloadTask?

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

任务下载

less 复制代码
let session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)

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

继续下载

php 复制代码
let session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)

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

取消下载

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

2.数据持久化

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

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

ini 复制代码
let fileManager = FileManager.default

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

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

ini 复制代码
let folderName = documentDaiectory.appendingPathComponent("MyVideos")

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

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

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

写入文件

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

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

php 复制代码
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
}

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

csharp 复制代码
if isFileExist() == true {
   // 继续下载
} else {
  // 重新下载
}

3.URLSessionDownloadDelegate

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

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

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

swift 复制代码
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)")
   }
}

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

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

核心代码

swift 复制代码
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)")
        }
    }
}
swift 复制代码
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)
        }
    }
}
相关推荐
终端安全笔记12 小时前
描述文件、企业证书、MDM 不是三个名字,是三层
android·ios·智能手机
传奇开心果编程12 小时前
【SwiftUI入门练中学】第1课 从零开始
学习·ui·ios·swiftui·swift
丿Wayne18 小时前
iOS Token 刷新实战(Actor 避免并发重复刷新)
ios·swift·token·actor·swift并发
终端安全笔记2 天前
iOS 27 给了租赁一个新工具,但它只认受监督的设备
android·网络·安全·ios
大熊猫侯佩2 天前
独立 App 首发完成 iPhone Duo 展开态适配
ios·swiftui·swift
光影少年2 天前
setImmediate 和 setTimeout(0) 的区别
android·前端·react.js·ios·前端框架
2501_916008892 天前
SSE 和 gRPC 流式接口如何抓包,调试 SSE 与 gRPC 流式接口
网络协议·计算机网络·网络安全·ios·adb·https·udp
HouWan2 天前
Flutter: MediaQuery.of(context) 为什么可能拖慢页面?
android·flutter·ios
SoaringHeart2 天前
Swift 进阶&封装:仿 Get 给SwiftUI 添加路由导航能力
swiftui·swift
用户38034165882972 天前
Broadcast Extension 里的 Live Activity:一次跨进程妥协
ios·音视频开发