Swift中URLSession任务存属性时的内存泄漏差异疑问
问题
研究Swift内存泄漏场景时遇到一个疑惑:创建新的UIViewController并调用fetchAPI函数,将未启动的URLSession下载任务存入控制器属性后关闭该控制器,发现控制器的deinit函数未被调用(存在内存泄漏),代码如下:
func fetchAPI() { let url = URL(string: "https://www.google.com")! let task = URLSession.shared.downloadTask(with: url) { _, _, _ in DispatchQueue.main.async { print(self.view.description) } } self.vcTask = task }
但如果调用fetchAPI时同时调用task.resume()启动任务,再关闭UIViewController,控制器的deinit函数能正常被调用(无内存泄漏),代码如下:
func fetchAPI() { let url = URL(string: "https://www.google.com")! let task = URLSession.shared.downloadTask(with: url) { _, _, _ in DispatchQueue.main.async { print(self.view.description) } } self.vcTask = task task.resume() // start downloading }
原本认为将任务存入UIViewController属性且回调中使用self会形成循环引用导致内存泄漏,但为何启动任务后不会出现内存泄漏?
分析与解答
核心原因在于未启动的URLSessionTask和已启动完成的Task对self的持有逻辑完全不同:
未启动的Task的持有链
当你创建downloadTask但不调用resume()时:- UIViewController通过属性
vcTask强持有Task - 未启动的Task会强持有其回调闭包,而闭包中直接引用了
self(UIViewController) - 同时,
URLSession.shared会一直强持有未启动的Task(Task属于该Session,未启动状态下Session不会释放它)
这就形成了闭环的循环引用链:UIViewController → vcTask → 闭包 → UIViewController,再加上Session对Task的额外强持有,整个引用链无法被ARC自动回收,所以控制器的deinit不会触发。
- UIViewController通过属性
启动并完成后的Task的持有变化
当调用task.resume()后:- Task启动执行,完成(或失败、被取消)后,
URLSession.shared会主动释放对该Task的强引用 - 同时,Task在回调闭包执行完毕后,也会释放对闭包的强引用
此时循环引用链被彻底打破:UIViewController虽仍持有vcTask,但Task不再持有闭包,Session也不再持有Task,当控制器被关闭时,没有其他外部强引用持有它,ARC会正常回收控制器,触发deinit。
- Task启动执行,完成(或失败、被取消)后,
另外要注意:无论Task是否启动,最稳妥的做法是在闭包中使用弱引用([weak self]),从根源上避免循环引用风险,示例代码如下:
let task = URLSession.shared.downloadTask(with: url) { [weak self] _, _, _ in DispatchQueue.main.async { guard let self = self else { return } print(self.view.description) } }
内容的提问来源于stack exchange,提问作者Thanapat Sorralump
相关产品推荐
相关产品推荐

