[英]How to prevent memory leak with using self in closure
我有下载文件的课程:
class FileDownloader {
private let downloadsSession = URLSession(configuration: .default)
private var task: URLSessionDownloadTask?
private let url: URL
init(url: URL) {
self.url = url
}
public func startDownload(){
download()
}
private func download(){
task = downloadsSession.downloadTask(with: url) {[weak self] (location, response, error) in
guard let weakSelf = self else {
assertionFailure("self was deallocated")
return }
weakSelf.saveDownload(sourceUrl: weakSelf.url, location: location, response: response, error: error)
}
task!.resume()
}
private func saveDownload(sourceUrl : URL, location : URL?, response : URLResponse?, error : Error?) {
if error != nil {
assertionFailure("error \(String(describing: error?.localizedDescription))")
return }
let destinationURL = localFilePath(for: sourceUrl)
let fileManager = FileManager.default
try? fileManager.removeItem(at: destinationURL)
do {
try fileManager.copyItem(at: location!, to: destinationURL)
print("save was completed at \(destinationURL) from \(String(describing: location))")
} catch let error {
print("Could not copy file to disk: \(error.localizedDescription)")
}
}
private func localFilePath(for url: URL) -> URL {
let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
return documentsPath.appendingPathComponent(url.lastPathComponent)
}
}
当我调用startDownload()
,在以下行进行调试时出现错误:
assertionFailure("self was deallocated")
当我将下载功能更改为此:
private func download(){
task = downloadsSession.downloadTask(with: url) {(location, response, error) in
self.saveDownload(sourceUrl: self.url, location: location, response: response, error: error)
}
task!.resume()
}
一切正常,但恐怕可能导致未正确释放到内存中的对象出现问题。 如何避免这种情况? 我做对了吗?
首先,为什么会导致断言失败? 因为您要让FileDownloader
实例超出范围。 您尚未共享调用方式,但是您很可能将其用作局部变量。 如果您解决了该问题,那么您的问题就消失了。
其次,当您更改实现以删除[weak self]
模式时,您并没有强大的参考周期,而是只是指示它在完成下载之前不要释放FileDownloader
。 如果这是您想要的行为,那很好。 说“在异步任务完成之前让它一直保持对自身的引用”是一种完全可以接受的模式。实际上,这正是URLSessionTask
所做的。 显然,关于[weak self]
模式的含义,您需要绝对清楚,因为在某些情况下它会引入强大的参考周期,但在这种情况下不会。
只有当您有两个对象之间具有持久的强引用时,才会发生强引用循环(有时可能涉及两个以上的对象)。 对于URLSession
,下载完成后,Apple会谨慎地编写downloadTask
方法,以便它在调用闭包后显式释放闭包,从而解决任何潜在的强引用周期。
例如,考虑以下示例:
class Foo {
func performAfterFiveSeconds(block: @escaping () -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
self.doSomething()
block()
}
}
func doSomething() { ... }
}
上面的方法很好,因为asyncAfter
在运行时会释放闭包。 但是考虑以下示例,我们将闭包保存在自己的ivar中:
class BarBad {
private var handler: (() -> Void)?
func performAfterFiveSeconds(block: @escaping () -> Void) {
handler = block
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
self.calledWhenDone()
}
}
func calledWhenDone() {
// do some stuff
doSomething()
// when done, call handler
handler?()
}
func doSomething() { ... }
}
现在这是一个潜在的问题,因为这一次我们将闭包保存在一个ivar中,为闭包创建了强大的引用,并引入了经典的强引用周期的风险。
但幸运的是,这很容易解决:
class BarGood {
private var handler: (() -> Void)?
func performAfterFiveSeconds(block: @escaping () -> Void) {
handler = block
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
self.calledWhenDone()
}
}
func calledWhenDone() {
// do some stuff
doSomething()
// when done, call handler
handler?()
// make sure to release handler when done with it to prevent strong reference cycle
handler = nil
}
func doSomething() { ... }
}
当将handler
设置为nil
时,这解决了强引用循环。 这实际上就是URLSession
(以及GCD方法,如async
或asyncAfter
)所做的。 他们保存闭包,直到调用它,然后释放它。
而不是使用此:
task = downloadsSession.downloadTask(with: url) {(location, response, error) in
self.saveDownload(sourceUrl: self.url, location: location, response: response, error: error)
}
将其移至URLSessionDownloadTask和URLSession的委托中
class FileDownloader:URLSessionTaskDelegate, URLSessionDownloadDelegate
并实现其方法:
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
if totalBytesExpectedToWrite > 0 {
let progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
debugPrint("Progress \(downloadTask) \(progress)")
}
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
debugPrint("Download finished: \(location)")
try? FileManager.default.removeItem(at: location)
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
debugPrint("Task completed: \(task), error: \(error)")
}
我知道这个值不会为零,但要避免避免强制展开:
task!.resume()
下载任务直接将服务器的响应数据写入一个临时文件,以便在数据从服务器到达时为您的应用程序提供进度更新。 在后台会话中使用下载任务时,即使您的应用已暂停或未运行,这些下载仍会继续。
您可以暂停(取消)下载任务并稍后再恢复(假设服务器支持这样做)。 您也可以恢复由于网络连接问题而失败的下载。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.