如何在闭包中使用 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(以及像 asyncasyncAfter 这样的 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 来防止内存泄漏的主要内容,如果未能解决你的问题,请参考以下文章

Swift 结构内存泄漏

转《js闭包与内存泄漏》

Echarts 如何防止内存泄漏

闭包会造成内存泄漏吗?

如何防止 CompileAssemblyFromSource 泄漏内存?

JavaScript内存管理闭包和内存泄漏