繁体   English   中英

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

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

我有下载文件的课程:

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

一切正常,但恐怕可能导致未正确释放到内存中的对象出现问题。 如何避免这种情况? 我做对了吗?

首先,为什么会导致断言失败? 因为您要让FileDownloader实例超出范围。 您尚未共享调用方式,但是您很可能将其用作局部变量。 如果您解决了该问题,那么您的问题就消失了。

其次,当您更改实现以删除[weak self]模式时,您并没有强大的参考周期,而是只是指示它在完成下载之前不要释放FileDownloader 如果这是您想要的行为,那很好。 说“在异步任务完成之前让它一直保持对自身的引用”是一种完全可以接受的模式。实际上,这正是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 (以及GCD方法,如asyncasyncAfter )所做的。 他们保存闭包,直到调用它,然后释放它。

而不是使用此:

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

我知道这个值不会为零,但要避免避免强制展开:

task!.resume()

下载任务直接将服务器的响应数据写入一个临时文件,以便在数据从服务器到达时为您的应用程序提供进度更新。 在后台会话中使用下载任务时,即使您的应用已暂停或未运行,这些下载仍会继续。

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

暂无
暂无

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

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