普通视图

发现新文章,点击刷新页面。
今天 — 2025年7月5日掘金 iOS

iOS断点下载

作者 RyanGo
2025年7月4日 17:25

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

知识点:

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

关于OC与Swift内存管理的解惑

作者 丶皮蛋菌
2025年7月4日 14:01
  • 在Swift中,如何解决闭包的循环引用?
myClosure = { [weak self] in
    guard let self = self else { return }
}
  • 那么如果是多个闭包嵌套呢?
// ✅ 只需要在最外层写一次 
myClosure1 = { [weak self] in [weak self]
    // 在顶部进行一次安全解包
    guard let self = self else { return }
    // 在这个作用域里,`self` 是一个临时的强引用,可以安全使用
    // 不用担心它被提前释放
    myClosure2 = {
        // 它会捕获上面 guard let 创建好的、非可选的强引用 self
        myClosure3 = {
            // 你可以直接、安全地调用 self 的方法或属性
        }
    }
}
  • 那么又有一个问题:在OC中的block,就需要每一层都要弱引用,第二层要先强引用再弱引用,第三层再先强引用再弱引用吧?为什么Swift不是这个逻辑? 这是因为Swift 的逻辑确实和 OC 不一样,它在这方面做了极大的简化和安全性提升。

OC 的核心问题在于:   @strongify(self) 创建的那个临时强引用 strongSelf 的生命周期会持续到包含它的那个 block 执行完毕。如果这个 block 内部又启动了一个长时间的异步任务(第二个 block),那么 strongSelf 会被第二个 block 捕获,导致 self 实例的生命周期被不必要地延长。因此,为了追求最精细的内存管理,开发者才会在每一层异步调用前都重复进行“弱化 -> 强化”的操作。

Swift 的优势在于:  guard let self = self 创建的强引用 self 是一个全新的、仅存在于外层闭包作用域内的局部变量。当 anotherAsyncTask 的闭包(内层闭包)创建时,它捕获的是这个局部的、新的 self。一旦外层闭包执行完毕(someAsyncTask 的回调结束),这个局部的 self 变量就会被销毁。anotherAsyncTask 的闭包对它的持有也就自然解除了,完全不会影响到原始 self 实例的生命周期。

特性 / 行为 Objective-C (block) Swift (closure)
弱引用声明 @weakify(self) 或 __weak typeof(self) weakSelf = self; 在捕获列表 [weak self]
临时强引用 @strongify(self) 或 __strong typeof(weakSelf) strongSelf = weakSelf; guard let self = self else { return }
嵌套捕获 内层 block 捕获由 @strongify 创建的 strongSelf,其生命周期可能过长,导致需要“再次弱化”。 内层闭包捕获由 guard let 创建的局部强引用 self。该局部变量生命周期很短,因此无需再次弱化
开发者操作 需要警惕并可能在每一层异步调用前都重复“弱化-强化”的模式。 只需要在最外层做一次“弱化-强化” ,内部可以完全放心使用。
  • 但是在实际开发中,swift代码提示引用报错,往往xcode是这样解决的,为什么?
someClosure = { [self] in }

那是因为Xcode 的首要任务是解决编译错误,而不是帮你分析内存管理。  而 [self] in 正是解决这个特定编译错误的“最直接”的语法。它解决了语法问题,但它没有解决循环引用的问题。它只是把一个隐式的强引用,变成了一个显式的强引用。所以解决循环引用问题还是需要[weak self]

  • 那么在实际开发中[self]对于内存泄露来说是错误的呗?

这个说法不完全准确,但您的警惕性非常对!更精确的说法是: 在【会】产生循环引用的场景下使用 [self],是绝对错误的,它会直接导致内存泄漏。 但是,在【不会】产生循环引用的场景下,[self] 则是安全、甚至是被推荐的写法。 所以,[self] 本身不是“错误”,它只是一个工具。错误的是在不合适的场景下使用了这个工具。

比如下列两种情况

// 这个闭包被传递给 UIView.animate,执行完动画后就会被销毁。 
// self 并没有一个属性来持有这个闭包。 
// 所以 self -> 闭包 这条强引用链不存在。 
UIView.animate(withDuration: 0.5) { [self] in 
    // 在这里使用 [self] in 是【完全正确】的。 
    // 它明确地告诉编译器:“我知道我在强引用 self,且我确定这是安全的。” 
}

// DispatchQueue.main.asyncAfter 的闭包同样是执行完就销毁。 
DispatchQueue.main.asyncAfter(deadline: .now() +1.0) { [self] in 
    // 这里使用 [self] in 也是【完全正确】的。 
}

简单的可以理解为“一次性”的工作 (用 [self] 是安全的),“长期”的规则 (必须用 [weak self])。

  • 那么又有一个场景,在对网络请求进行二次封装的情况下,在调用网络请求时,是否需要弱引用?

答案是不需要的。为什么呢?因为Alamofire临时持有了您的闭包,由于 ViewController 没有持有任何东西,所以闭包无论如何强引用 ViewController,都构不成一个闭环。因此,这里使用 [self] 来显式强引用是完全安全的。

  • 为什么感觉 OC 的 AFNetworking 封装调用时不需要弱引用?

无论是在 OC 的 AFNetworking 还是 Swift 的 Alamofire,它们【内部都没有,也不可能】自动处理您在闭包中捕获 self 导致的循环引用。防止循环引用的责任始终在调用者(也就是您)身上。

  • 那为什么您会有“AFN不需要弱引用”的印象呢?

原因和我们上面分析的完全一样:因为您在 OC 中调用 AFN 封装的场景,很可能也属于“一次性”的调用,本身就不会产生循环引用。由于AFHTTPSessionManager的实例 manager 是一个局部变量,方法执行完就释放了,success block 被 manager 临时持有,执行完也就释放了。所以,当时您在 OC 里不写弱引用是正确的,不是因为 AFNetworking 内部处理了,而是因为您的【用法】决定了它根本没有循环引用!

[MyOCNetworkManager requestWithURL:@"" parameters:nil success:^(id responseObject) { 
    // 您在这里直接使用 self,比如 [self.tableView reloadData]; 
    // 并没有写 __weak typeof(self) weakSelf = self; 
} failure:^(NSError *error) { 
    // ... 
}];
  • 那么在OC+AFNetworking的二次封装回调时,如果我依然写弱引用的话,会有问题吗?

完全没有问题,这样做在内存上是绝对安全的,但同样,它也是不必要的,并且有轻微的副作用。 在那个场景下,编写弱引用代码(即 weak/strong dance)是“安全但多余的”。

我们来对比一下两种写法:

  1. 在您调用一个“一次性”的网络请求封装时,闭包不会被您的 ViewController 持有,因此不存在循环引用。
[MyOCNetworkManager requestWithURL:@"" 
                        parameters:nil 
                           success:^(id responseObject) {
    // 直接使用 self,闭包会强引用 self。
    // 因为没有循环引用,self 会在闭包执行完后被正常释放,这是安全的。
    [self.tableView reloadData];
} 
                           failure:nil];

这种写法简洁、清晰,并且正确地表达了意图:“这是一个一次性的任务,我需要 self 在任务执行时是存在的。”

  1. 如果您坚持使用弱引用,代码会是这样:
__weak typeof(self) weakSelf = self;
[MyOCNetworkManager requestWithURL:@"/some/path" 
                        parameters:nil 
                           success:^(id responseObject) {
    __strong typeof(weakSelf) strongSelf = weakSelf;
    if (!strongSelf) {
        return;
    }
    // 使用 strongSelf 来确保在闭包执行期间 self 不会被释放
    [strongSelf.tableView reloadData];
} 
                           failure:nil];
  1. 内存安全吗?

是的,100%安全。弱引用永远不会增加引用计数,所以它绝不会“创造”出一个循环引用。从这个角度看,它没有任何“问题”。

  1. 有什么副作用或缺点吗?  

有,和 Swift 的情况完全相同:

代码变得冗余:为了一个没有必要的安全措施,您多写了三行样板代码 (__weak__strongif)。这降低了代码的简洁性。

意图变得模糊:当其他开发者读到这段代码时,他们会看到 weak/strong dance,这通常是一个强烈的信号,表示“这里有循环引用的风险”。他们可能会因此花时间去寻找一个实际并不存在的风险点,增加了维护成本。

极端情况下的功能差异:如果 self (比如一个 UIViewController) 在网络请求发出后、但在回调执行前被释放了(例如用户快速返回上一个页面),那么:

写法一(强引用) :self 会被闭包“续命”,直到闭包执行完毕。[self.tableView reloadData] 会被执行。

写法二(弱引用) :self 会被立即释放。当回调执行时,strongSelf 会是 nil,代码会直接 return[strongSelf.tableView reloadData] 不会被执行。

[MyOCNetworkManager requestWithURL:@"" 
                        parameters:nil 
                           success:^(id responseObject) {
    // 直接使用 self,闭包会强引用 self。
    // 因为没有循环引用,self 会在闭包执行完后被正常释放,这是安全的。
    [self.tableView reloadData];
} 
                           failure:nil];
 __weak typeof(self) weakSelf = self;
[MyOCNetworkManager requestWithURL:@"" 
                        parameters:nil 
                           success:^(id responseObject) {
    __strong typeof(weakSelf) strongSelf = weakSelf;
    if (!strongSelf) {
        return;
    }
    // 使用 strongSelf 来确保在闭包执行期间 self 不会被释放
    [strongSelf.tableView reloadData];
} 
                           failure:nil];
  • 极端情况下的功能差异:如果 self (比如一个 UIViewController) 在网络请求发出后、但在回调执行前被释放了(例如用户快速返回上一个页面),写法二(弱引用):self 会被立即释放。当回调执行时,strongSelf 会是 nil,代码会直接 return,[strongSelf.tableView reloadData] 不会被执行。 但是实际中的这种写法很常见,我以前就是这么写,但是没有发现什么问题啊?返回页面还是会执行dealloc‌,为什么?
  1. 大多数情况下,行为差异“无关紧要”

我们回到最常见的场景:网络请求回来后,更新界面。

Objective-C

// 弱引用写法
__weak typeof(self) weakSelf = self;
[MyNetworkManager request:^{
    __strong typeof(weakSelf) strongSelf = weakSelf;
    if (!strongSelf) {
        return; // 如果 self 销毁了,就直接返回
    }
    [strongSelf.tableView reloadData]; // 更新界面
}];

设想一下用户快速返回,self (也就是 ViewController) 被销毁了。这时网络回调回来了,因为 strongSelf 是 nil,所以 [strongSelf.tableView reloadData] 这行代码没有被执行**。

这对用户来说是问题吗?完全不是。  因为界面都已经消失了,tableView 也不存在了,去刷新一个不存在的界面本来就是一件没有意义的事情。代码不执行,反而更干净利落。

所以,在 99% 的 UI 更新场景中,弱引用导致的“代码不执行”这个行为差异,不仅不是问题,反而是我们期望的、最合理的结果。

  1. “快速返回”的极端情况发生概率低

要触发这个“功能差异”,需要满足一个条件:self 的销毁发生在“网络请求发出后”和“回调执行前”这个短暂的时间窗口内。

对于大多数响应速度很快的 API 来说,这个窗口可能只有几百毫秒。用户需要操作得非常快才能正好卡在这个时间点上。因此,在日常测试和使用中,这个情况本身就不容易遇到。

  1. 即使代码不执行,也无可见负面影响

假设回调里做的事情是 [self hideLoadingIndicator]。如果用户已经返回了上一个页面,那个加载指示器 loadingIndicator 本来就已经随着页面消失了,所以 hideLoadingIndicator 这行代码执不执行,用户根本感知不到任何区别。

❌
❌