【问题标题】:How to prevent memory leak with using self in closure如何在闭包中使用 self 来防止内存泄漏
【发布时间】: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()
    }

一切都很好,但我担心它可能会导致对象在内存中未正确释放的问题。如何避免这种情况?我做的对吗?

【问题讨论】:

    标签: ios swift


    【解决方案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()
    

    下载任务直接将服务器的响应数据写入一个 临时文件,为您的应用程序提供进度更新作为数据 从服务器到达。当您在后台使用下载任务时 会话,即使您的应用被暂停或 否则不会运行。

    您可以暂停(取消)下载任务并稍后恢复(假设 服务器支持这样做)。您还可以恢复下载 由于网络连接问题而失败。

    【讨论】:

      猜你喜欢
      • 2015-06-11
      • 2019-10-17
      • 1970-01-01
      • 2010-12-20
      • 2021-10-22
      • 1970-01-01
      • 1970-01-01
      • 2013-07-12
      相关资源
      最近更新 更多