Swift4URLSession.downloadTask内存循环如何解决?
How to resolve memory cycle in Swift 4 URLSession.downloadTask?
我有一个基本的 viewController
,只有一个按钮,当点击该按钮时,它会调用一个方法开始从给定的有效 url.
下载图像
我非常小心地将任何强指针传递给 viewcontroller。而且我能够关闭 viewcontroller 并毫无问题地返回到呈现控制器。然而,由 viewcontroller 创建的 Web()
的对象实例不会被释放,即使 viewcontroller 的 deinit
被调用。
我做错了什么?
顺便说一句:文件下载开始,报告进度并报告文件位置。但是,一旦文件下载完成,该对象就永远不会被释放。
viewController.swift
@IBAction func buttonTapped(_ sender: UIButton) {
//first create an instance of Web class (download helper)
let web = Web(url: "https://www.google.com.sa/images/branding/googlelogo/2x/googlelogo_color_120x44dp.png")
// all call back closures have no reference to self
web.downloadFile(
finishedHandler: {fileLocation in print("file stored to \(fileLocation)")},
progressHandler: {bytes,total in print("written \(bytes) out of \(total)")},
errorHandler: {msg in print("error: \(msg)")}
)
}
并且我已将 Web()
定义为 NSObject
,它符合 URLSessionDownloadDelegate
以利用委托事件方法(didFinishDownload
/ didWriteBytes
等) .
Web.swift
import UIKit
class Web : NSObject {
var urlToDownload : String?
// the following variables are references to closures passed to the object.
var progressCallback : ((_ bytesWritten:Int64, _ totalExpectedBytes: Int64)->())?
var finishedCallback : ((_ fileLocation: String)->())?
static var instanceCount = 0 // keep track of number of instances created
init(url: String) {
Web.instanceCount += 1
urlToDownload = url
print(" new instance of Web created. Total : \(Web.instanceCount)")
}
deinit {
Web.instanceCount -= 1
print("Web instance deallocated. Remaining: \(Web.instanceCount)")
}
}
并且我将文件下载逻辑添加为同一文件的扩展:
extension Web : URLSessionDownloadDelegate {
func downloadFile(
finishedHandler: @escaping (_ fileLocation:String)->(),
progressHandler: @escaping (_ bytesWritten:Int64, _ totalBytes: Int64)->(),
errorHandler: @escaping (_ errorMsg:String)->()) {
// we need to capture the closure because, these will
// be called once the delegate methods are triggered
self.progressCallback = progressHandler
self.finishedCallback = finishedHandler
if let url = URL(string: self.urlToDownload!) {
let session = URLSession(
configuration: .default,
delegate: self,
delegateQueue: nil)
let task = session.downloadTask(with: url)
task.resume()
}
}
// MARK :- Delegate methods
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
// call the closure if it still exists
self.finishedCallback?(location.absoluteString)
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
// report progress by calling captured closure, if exists
self.progressCallback?(totalBytesWritten,totalBytesExpectedToWrite)
}
}
在我追踪问题之后(使用 Memory Graph Debugger),我发现问题的原因在这段代码中(Web class - 下载文件方法):
if let url = URL(string: self.urlToDownload!) {
let session = URLSession(
configuration: .default,
delegate: self,
delegateQueue: nil)
let task = session.downloadTask(with: url)
task.resume()
}
实际上有一个与实例化相关的问题URLSession
没有摆脱它;因此,你必须做一种 - 手动处理来解决它,调用 finishTasksAndInvalidate()
适合这样的问题:
if let url = URL(string: self.urlToDownload!) {
let session = URLSession(
configuration: .default,
delegate: self,
delegateQueue: nil)
let task = session.downloadTask(with: url)
task.resume()
// here we go:
session.finishTasksAndInvalidate()
}
为了确保它按预期工作,我建议在 Web class 中的委托方法和 deinit
添加断点,您应该会看到它按预期工作(委托方法会工作,然后 deinit
会被调用)。
此外:
如果您想了解更多关于如何使用 Memory Graph Debugger 的信息,您可以查看:
使用 Memory Graph Debugger 进行调试时您可以清楚地看到的问题是对象 Web
被 URLSession
对象持有CFXURLCache
反过来阻止了 URLSession
。他们彼此有很强的参考。因此只有当会话或缓存从内存中释放时,Web
对象才会从内存中释放。
解决方案很简单,您需要在下载完成后使会话无效以打破NSURLSession
和CFXURLCache
之间的循环引用:
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
// call the closure if it still exists
self.finishedCallback?(location.absoluteString)
session.finishTasksAndInvalidate()
}
问题在于 URLSession 保留了它的委托,因此在充当其 delegate
的同一对象中存储对 URLSession 的强引用是循环的。
如您所知,您可以通过使 URLSession 失效来破坏强引用。但总的来说,简单的解决方案是:首先不要创建循环。
我有一个基本的 viewController
,只有一个按钮,当点击该按钮时,它会调用一个方法开始从给定的有效 url.
我非常小心地将任何强指针传递给 viewcontroller。而且我能够关闭 viewcontroller 并毫无问题地返回到呈现控制器。然而,由 viewcontroller 创建的 Web()
的对象实例不会被释放,即使 viewcontroller 的 deinit
被调用。
我做错了什么?
顺便说一句:文件下载开始,报告进度并报告文件位置。但是,一旦文件下载完成,该对象就永远不会被释放。
viewController.swift
@IBAction func buttonTapped(_ sender: UIButton) {
//first create an instance of Web class (download helper)
let web = Web(url: "https://www.google.com.sa/images/branding/googlelogo/2x/googlelogo_color_120x44dp.png")
// all call back closures have no reference to self
web.downloadFile(
finishedHandler: {fileLocation in print("file stored to \(fileLocation)")},
progressHandler: {bytes,total in print("written \(bytes) out of \(total)")},
errorHandler: {msg in print("error: \(msg)")}
)
}
并且我已将 Web()
定义为 NSObject
,它符合 URLSessionDownloadDelegate
以利用委托事件方法(didFinishDownload
/ didWriteBytes
等) .
Web.swift
import UIKit
class Web : NSObject {
var urlToDownload : String?
// the following variables are references to closures passed to the object.
var progressCallback : ((_ bytesWritten:Int64, _ totalExpectedBytes: Int64)->())?
var finishedCallback : ((_ fileLocation: String)->())?
static var instanceCount = 0 // keep track of number of instances created
init(url: String) {
Web.instanceCount += 1
urlToDownload = url
print(" new instance of Web created. Total : \(Web.instanceCount)")
}
deinit {
Web.instanceCount -= 1
print("Web instance deallocated. Remaining: \(Web.instanceCount)")
}
}
并且我将文件下载逻辑添加为同一文件的扩展:
extension Web : URLSessionDownloadDelegate {
func downloadFile(
finishedHandler: @escaping (_ fileLocation:String)->(),
progressHandler: @escaping (_ bytesWritten:Int64, _ totalBytes: Int64)->(),
errorHandler: @escaping (_ errorMsg:String)->()) {
// we need to capture the closure because, these will
// be called once the delegate methods are triggered
self.progressCallback = progressHandler
self.finishedCallback = finishedHandler
if let url = URL(string: self.urlToDownload!) {
let session = URLSession(
configuration: .default,
delegate: self,
delegateQueue: nil)
let task = session.downloadTask(with: url)
task.resume()
}
}
// MARK :- Delegate methods
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
// call the closure if it still exists
self.finishedCallback?(location.absoluteString)
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
// report progress by calling captured closure, if exists
self.progressCallback?(totalBytesWritten,totalBytesExpectedToWrite)
}
}
在我追踪问题之后(使用 Memory Graph Debugger),我发现问题的原因在这段代码中(Web class - 下载文件方法):
if let url = URL(string: self.urlToDownload!) {
let session = URLSession(
configuration: .default,
delegate: self,
delegateQueue: nil)
let task = session.downloadTask(with: url)
task.resume()
}
实际上有一个与实例化相关的问题URLSession
没有摆脱它;因此,你必须做一种 - 手动处理来解决它,调用 finishTasksAndInvalidate()
适合这样的问题:
if let url = URL(string: self.urlToDownload!) {
let session = URLSession(
configuration: .default,
delegate: self,
delegateQueue: nil)
let task = session.downloadTask(with: url)
task.resume()
// here we go:
session.finishTasksAndInvalidate()
}
为了确保它按预期工作,我建议在 Web class 中的委托方法和 deinit
添加断点,您应该会看到它按预期工作(委托方法会工作,然后 deinit
会被调用)。
此外:
如果您想了解更多关于如何使用 Memory Graph Debugger 的信息,您可以查看:
使用 Memory Graph Debugger 进行调试时您可以清楚地看到的问题是对象 Web
被 URLSession
对象持有CFXURLCache
反过来阻止了 URLSession
。他们彼此有很强的参考。因此只有当会话或缓存从内存中释放时,Web
对象才会从内存中释放。
解决方案很简单,您需要在下载完成后使会话无效以打破NSURLSession
和CFXURLCache
之间的循环引用:
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
// call the closure if it still exists
self.finishedCallback?(location.absoluteString)
session.finishTasksAndInvalidate()
}
问题在于 URLSession 保留了它的委托,因此在充当其 delegate
的同一对象中存储对 URLSession 的强引用是循环的。
如您所知,您可以通过使 URLSession 失效来破坏强引用。但总的来说,简单的解决方案是:首先不要创建循环。