[英]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方法,如async
或asyncAfter
)所做的。 他們保存閉包,直到調用它,然后釋放它。
而不是使用此:
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.