简体   繁体   English

如何通过使用自闭锁来防止内存泄漏

[英]How to prevent memory leak with using self in closure

I have class for download files: 我有下载文件的课程:

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

When i call startDownload() i get an error at debugging at line: 当我调用startDownload() ,在以下行进行调试时出现错误:

assertionFailure("self was deallocated")

When i change download function to this: 当我将下载功能更改为此:

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

All work nice, but i'm afraid it may cause problems with object that is not properly released in memory. 一切正常,但恐怕可能导致未正确释放到内存中的对象出现问题。 How to avoid such situation? 如何避免这种情况? Am i doing things right? 我做对了吗?

First, why are you getting that assertion failure? 首先,为什么会导致断言失败? Because you're letting FileDownloader instance fall out of scope. 因为您要让FileDownloader实例超出范围。 You haven't shared how you're invoking this, but you're likely using it as a local variable. 您尚未共享调用方式,但是您很可能将其用作局部变量。 If you fix that, your problem goes away. 如果您解决了该问题,那么您的问题就消失了。

Second, when you changed your implementation to remove the [weak self] pattern, you don't have a strong reference cycle, but rather you've just instructed it to not release the FileDownloader until the download is done. 其次,当您更改实现以删除[weak self]模式时,您并没有强大的参考周期,而是只是指示它在完成下载之前不要释放FileDownloader If that's the behavior you want, then that's fine. 如果这是您想要的行为,那很好。 It's a perfectly acceptable pattern to say “have this keep a reference to itself until the asynchronous task is done.” In fact, that's precisely what URLSessionTask does. 说“在异步任务完成之前让它一直保持对自身的引用”是一种完全可以接受的模式。实际上,这正是URLSessionTask所做的。 Clearly, you need to be absolutely clear regarding the implications of omitting the [weak self] pattern, as in some cases it can introduce a strong reference cycle, but not in this case. 显然,关于[weak self]模式的含义,您需要绝对清楚,因为在某些情况下它会引入强大的参考周期,但在这种情况下不会。


Strong reference cycles only occur when you have two objects with persistent strong references to each other (or sometimes more than two objects can be involved). 只有当您有两个对象之间具有持久的强引用时,才会发生强引用循环(有时可能涉及两个以上的对象)。 In the case of URLSession , when the download is done, Apple prudently wrote downloadTask method so that it explicitly releases the closure after calling it, resolving any potential strong reference cycle. 对于URLSession ,下载完成后,Apple会谨慎地编写downloadTask方法,以便它在调用闭包后显式释放闭包,从而解决任何潜在的强引用周期。

For example, consider this example: 例如,考虑以下示例:

class Foo {
    func performAfterFiveSeconds(block: @escaping () -> Void) {
        DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
            self.doSomething()

            block()
        }
    }

    func doSomething() { ... }
}

The above is fine because asyncAfter releases the closure when it's run. 上面的方法很好,因为asyncAfter在运行时会释放闭包。 But consider this example where we save the closure in our own ivar: 但是考虑以下示例,我们将闭包保存在自己的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() { ... }
}

Now this is a potential problem, because this time we save the closure in an ivar, creating a strong reference to the closure, and introducing risk of a classic strong reference cycle. 现在这是一个潜在的问题,因为这一次我们将闭包保存在一个ivar中,为闭包创建了强大的引用,并引入了经典的强引用周期的风险。

But fortunately this is easily remedied: 但幸运的是,这很容易解决:

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

This resolves the strong reference cycle when it sets handler to nil . 当将handler设置为nil时,这解决了强引用循环。 This is effectively what URLSession (and GCD methods like async or asyncAfter ) do. 这实际上就是URLSession (以及GCD方法,如asyncasyncAfter )所做的。 They save the closure until they call it, and then they release it. 他们保存闭包,直到调用它,然后释放它。

Instead of use this: 而不是使用此:

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

move it to delegates of URLSessionDownloadTask and URLSession 将其移至URLSessionDownloadTask和URLSession的委托中

class FileDownloader:URLSessionTaskDelegate, URLSessionDownloadDelegate

and implement its methods: 并实现其方法:

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

I know that this value won't be nil but try to avoid forcing unwrapping: 我知道这个值不会为零,但要避免避免强制展开:

task!.resume()

Download tasks directly write the server's response data to a temporary file, providing your app with progress updates as data arrives from the server. 下载任务直接将服务器的响应数据写入一个临时文件,以便在数据从服务器到达时为您的应用程序提供进度更新。 When you use download tasks in background sessions, these downloads continue even when your app is suspended or is otherwise not running. 在后台会话中使用下载任务时,即使您的应用已暂停或未运行,这些下载仍会继续。

You can pause (cancel) download tasks and resume them later (assuming the server supports doing so). 您可以暂停(取消)下载任务并稍后再恢复(假设服务器支持这样做)。 You can also resume downloads that failed because of network connectivity problems. 您也可以恢复由于网络连接问题而失败的下载。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM