[譯] Swift:通過示例避免記憶體洩漏

little_xia發表於2019-02-18

[譯] Swift:通過示例避免記憶體洩漏

在 Swift 中,使用自動引用計數(ARC)來管理 iOS 應用程式中的記憶體使用情況。

每次建立類的新例項時,ARC都會分配一塊記憶體來儲存有關它的資訊,並在不再需要該例項時自動釋放該記憶體。

作為開發人員,你不需要為記憶體管理做任何事情,除了以下3種情況,你需要告訴 ARC 有關例項之間關係的更多資訊,以避免「迴圈引用」。

在本文中,我們將在集中討論這3種情況,並檢視迴圈引用的實際示例以及如何去避免它們。

但是首先,我們得知道什麼是迴圈引用以及為什麼我們需要避免它們?


迴圈引用:

迴圈引用就是這種情況,兩個物件彼此具有強引用並相互持有,ARC 無法從記憶體中釋放這些物件從而導致「記憶體洩漏」。

在應用程式中出現記憶體洩漏是非常危險的,因為它們會影響應用程式的效能,並且在應用程式記憶體不足時可能會導致崩潰。


以下三種情況會造成記憶體洩漏:

1- 兩個類之間的強引用:

假設我們有2個類(Author 類和 Book 類)直接相互引用:

class Author {
    var name:String
    var book:Book
    
    init(name:String,book:Book) {
        self.name = name
        self.book = book
        print("Author Object was allocated in memory")
    }
    deinit {
        print("Author Object was de allocated")
    }
}

var author = Author(name:"John",book:Book())
author = nil
複製程式碼
class Book {
    var name:String
    var author:Author
    
    init(name:String,author:Author) {
        self.name = name
        self.author = author
        print("Book object was allocated in memory")
    }
    deinit {
        print("Book Object was deallocated")
    }
}
var book = Book(name:"Swift",author:author)
book = nil
複製程式碼

理論上,因為這兩個物件都被設定為 nil,所以應該先列印出兩個物件都已分配,然後列印出兩個物件都被銷燬,但是它會列印以下內容:

Author Object was allocated in memory
Book object was allocated in memory
複製程式碼

正如你所見,兩個物件並未從記憶體中釋放,因為當兩個物件之間彼此具有強引用時發生了迴圈引用。

為了解決這個問題,我們可以如下宣告弱引用或無主引用:

class Author {
   var name:String
   weak var book:Book? // book 物件需要被宣告為弱的可選項
    
    init(name:String,book:Book?) {
        self.name = name
        self.book = book
        print("Author Object was allocated in memory")
    }
    deinit {
        print("Author Object was deallocated")
    }
}
複製程式碼

這次兩個物件都會被釋放,控制檯將列印以下內容:

Author Object was allocated in memory
Book object was allocated in memory
Author Object was deallocated
Book Object was deallocated
複製程式碼

問題解決了,ARC 在清理記憶體塊時可以通過使其中一個引用變弱來釋放物件,但弱引用和無主引用是什麼呢?根據 apple 的文件:

弱引用

弱引用是一種不會強制保留它引用例項的引用,因此就不會阻止 ARC 處理這些的例項。這樣使引用避免了成為強引用迴圈的一部分。你可以通過在屬性或變數宣告之前放置 weak 關鍵字來標記弱引用。

無主引用

與弱引類似,無主引用 也不會對它引用的例項保持強引用。然而,與弱引用不同得是,當另一個例項具有相同的生命週期或更長的生命週期時,則需要使用無主引用。 你可以通過在屬性或變數宣告之前放置 unowned 關鍵字來標記無主引用。


2- 類協議關係:

記憶體洩漏的另一個原因可能是協議和類之間的密切關係。在下面的示例中,我們將採用一個真實的場景,我們有一個 TablViewController 類和一個 TableViewCell 類,當使用者按下 TableViewCell 中的一個按鈕時,它應該將此動作代理給 TablViewController,如下所示:

@objc protocol TableViewCellDelegate {
   func onAlertButtonPressed(cell:UITableViewCell)
}
class TableViewCell: UITableViewCell {

    var delegate:TableViewCellDelegate?
    
    @IBAction func onAlertButtonPressed(_ sender: UIButton) {
      delegate?.onAlertButtonPressed(cell: self)
    }
}
複製程式碼
class TableViewController: UITableViewController {

    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 10
    }
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath) as! TableViewCell
        cell.delegate = self
        return cell
    }
 
    deinit {
        print("TableViewController is deallocated")
    }

}
extension TableViewController: TableViewCellDelegate {
    func onAlertButtonPressed(cell: UITableViewCell) {
        if let row = tableView.indexPath(for: cell)?.row {
            print("cell selected at row: \(row)")
        }
        dismiss(animated: true, completion: nil)
    }
}
複製程式碼

通常,當我們關閉 TableViewController 時,ARC 應該呼叫 deinit 方法並且在控制檯中 列印「TableViewController is deallocated」,但是在這種情況下,由於 TableViewCellDelegate 和 TableViewController 彼此之間具有強引用,所以它們永遠不會從記憶體中釋放。

為了解決這個問題,我們可以簡單地將 TableViewCell 類調整為如下:

@objc protocol TableViewCellDelegate {
   func onAlertButtonPressed(cell:UITableViewCell)
}
class TableViewCell: UITableViewCell {

   weak var delegate:TableViewCellDelegate?
    
    @IBAction func onAlertButtonPressed(_ sender: UIButton) {
      delegate?.onAlertButtonPressed(cell: self)
    }
}
複製程式碼

這次關閉 TableViewController 就可以在控制檯中看到:

TableViewController is deallocated
複製程式碼

3- 閉包的強迴圈引用:

假設我們有以下 ViewController:

class ViewController: UIViewController {

    var closure : (() -> ()) = { }
  
    override func viewDidLoad() {
        super.viewDidLoad()
        closure = {
            self.view.backgroundColor = .red
        }
    }
    deinit {
        print("ViewController was deallocated")
    }
}
複製程式碼

嘗試關閉 ViewController,deinit 方法永遠不會被執行。 這是因為閉包捕獲了 ViewController 的強引用。要解決這個問題,我們需要在閉包中使用 weak 或 unowned 修飾的 self,如下所示:

class ViewController: UIViewController {

    var closure : (() -> ()) = { }
  
    override func viewDidLoad() {
        super.viewDidLoad()
        closure = { [unowned self] in
            self.view.backgroundColor = .red
        }
    }
    deinit {
        print("ViewController was deallocated")
    }
}
複製程式碼

這次關閉 ViewController 時控制檯將列印:

ClosureViewController was deallocated
複製程式碼

總結

毫無疑問,ARC 對應用程式的記憶體管理起了了不起的作用,我們開發者所要做的是注意類之間,類和協議之間以及內部閉包之間的強引用,通過宣告 weak 或者 unowned 來避免迴圈引用。


關於 ARC 的一些重要參考:

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章