簡體   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