如何在闭包中使用 self 来防止内存泄漏
Posted
技术标签:
【中文标题】如何在闭包中使用 self 来防止内存泄漏【英文标题】:How to prevent memory leak with using self in closure 【发布时间】:2019-06-23 04:01:33 【问题描述】:我有下载文件的课程:
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()
一切都很好,但我担心它可能会导致对象在内存中未正确释放的问题。如何避免这种情况?我做的对吗?
【问题讨论】:
【参考方案1】:首先,您为什么会遇到断言失败?因为您让FileDownloader
实例超出范围。你还没有分享你是如何调用它的,但你很可能将它用作局部变量。如果你解决了这个问题,你的问题就会消失。
其次,当您更改实现以删除 [weak self]
模式时,您没有强大的引用周期,而是您只是指示它在下载完成之前不要释放 FileDownloader
。如果这是你想要的行为,那很好。说“在异步任务完成之前让 this 保持对自身的引用”是一种完全可以接受的模式。事实上,这正是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
(以及像 async
或 asyncAfter
这样的 GCD 方法)所做的。他们保存闭包直到调用它,然后释放它。
【讨论】:
例如我在单元测试中这样使用它: func testDownload() let downloader = FileDownloader(url: URL(string: URLS.firstFileUrl.rawValue)!) downloader.startDownload() waitForExpectations(超时:大超时,处理程序:无) 在这种情况下,我应该担心强引用保留周期? @EvgeniyKleban - 当您的对象之间具有持久的强引用时,您需要担心强引用循环。在这种情况下,downloadTask
在调用闭包后释放闭包,解决任何循环。您需要担心的地方是我们知道它不会被释放的地方(例如,一个永不停止的重复计时器;我们有自己的闭包 ivar,当我们完成它时忽略将其设置为 nil
;我们有很强的委托参考;等等)。
如果你对强引用循环有顾虑,你可以运行你的实际应用程序,练习它,返回到应该释放相关对象的静止状态,然后点击“调试内存图”按钮@ 987654321@
顺便说一下,这里有一些关于你的代码 sn-p 的不相关的观察:gist.github.com/robertmryan/fd52d2dcc4cdbb8632d1bf59f598a342【参考方案2】:
不要使用这个:
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)")
我知道这个值不会是 nil 但尽量避免强制展开:
task!.resume()
下载任务直接将服务器的响应数据写入一个 临时文件,为您的应用程序提供进度更新作为数据 从服务器到达。当您在后台使用下载任务时 会话,即使您的应用被暂停或 否则不会运行。
您可以暂停(取消)下载任务并稍后恢复(假设 服务器支持这样做)。您还可以恢复下载 由于网络连接问题而失败。
【讨论】:
以上是关于如何在闭包中使用 self 来防止内存泄漏的主要内容,如果未能解决你的问题,请参考以下文章