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)
        }
    }
}
相关推荐
Frank学习路上5 小时前
【IOS】XCode创建firstapp并运行(成为IOS开发者)
开发语言·学习·ios·cocoa·xcode
瓜子三百克12 小时前
CALayer的异步处理
macos·ios·cocoa
吴Wu涛涛涛涛涛Tao12 小时前
一步到位:用 Very Good CLI × Bloc × go_router 打好 Flutter 工程地基
flutter·ios
杂雾无尘15 小时前
开发者必看:如何在 iOS 应用中完美实现动态自定义字体!
ios·swift·apple
kymjs张涛17 小时前
零一开源|前沿技术周报 #6
前端·ios·harmonyos
与火星的孩子对话1 天前
Unity进阶课程【六】Android、ios、Pad 终端设备打包局域网IP调试、USB调试、性能检测、控制台打印日志等、C#
android·unity·ios·c#·ip
恋猫de小郭2 天前
Flutter Widget Preview 功能已合并到 master,提前在体验毛坯的预览支持
android·flutter·ios
点金石游戏出海3 天前
每周资讯 | Krafton斥资750亿日元收购日本动画公司ADK;《崩坏:星穹铁道》新版本首日登顶iOS畅销榜
游戏·ios·业界资讯·apple·崩坏星穹铁道
旷世奇才李先生3 天前
Swift 安装使用教程
开发语言·ios·swift
90后的晨仔3 天前
Xcode16报错: SDK does not contain 'libarclite' at the path '/Applicati
ios