简体   繁体   中英

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:

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. 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. 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. 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.


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.

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. But consider this example where we save the closure in our own 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.

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 . This is effectively what URLSession (and GCD methods like async or asyncAfter ) do. 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

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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