代码之家  ›  专栏  ›  技术社区  ›  Evgeniy Kleban

如何使用self-in-closure防止内存泄漏

  •  2
  • Evgeniy Kleban  · 技术社区  · 7 年前

    我有下载文件的课程:

    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()
        }
    

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

    2 回复  |  直到 7 年前
        1
  •  3
  •   Rob Md Fahim Faez Abir    7 年前

    FileDownloader 实例不在范围内。您尚未分享如何调用此函数,但可能将其用作局部变量。如果你解决了这个问题,你的问题就会消失。

    第二,当您更改实现以删除 [weak self] 直到下载完成。如果这是你想要的行为,那就好了。这是一种完全可以接受的模式,即在异步任务完成之前,保持对自身的引用。事实上,这正是我想要的 URLSessionTask [虚弱的自我] 模式,在某些情况下,它可以引入一个强引用循环,但在本例中不是这样。


    强引用循环仅在两个对象之间具有持久的强引用时发生(或者有时可能涉及两个以上的对象)。就 URLSession 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() { ... }
    }
    

    但幸运的是,这很容易纠正:

    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 异步后 )做。他们保存闭包直到调用它,然后释放它。

        2
  •  1
  •   Camilo Lopez    7 年前

    不要使用这个:

    task = downloadsSession.downloadTask(with: url) {(location, response, error) in
                self.saveDownload(sourceUrl: self.url, location: location, response: response, error: error)
            }
    

    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()
    

    临时文件,将进度更新作为数据提供给应用程序 从服务器到达。在后台使用下载任务时 会话,即使您的应用程序被挂起或关闭,这些下载仍将继续

    您可以暂停(取消)下载任务,稍后再继续(假设 服务器支持这样做)。你也可以继续下载