為何時常看到 delegate 被宣告為 weak ? 以 cell 的 delegate 為例

開發 iOS App 時,我們時常利用 delegate 的技巧讓不同元件溝通,比方利用 delegate 將資料回傳到上一頁。

宣告 property delegate 時,我們時常看到 delegate 被宣告為 weak,就像下圖 table view 的 delegate。

將 delegate 宣告為 weak 主要是為了避免 memory leak。以下我們實際看一個造成 memory leak 的 delegate 例子。我們將在 cell 類別宣告 delegate,然後將 view controller 設為 cell 的 delegate。

下載 Apple 的 To Do App 範例

從 ‎Develop in Swift Data Collections Teacher Guide 下載範例研究。

開啟 ToDoList 專案

ToDoList 專案的路徑如下。

1 - Tables and Persistence/Guided Project - List/resources/ToDoList/ToDoList.xcodeproj

執行 App

點選 Todo 項目 cell 上的圈圈可設定項目是否已完成,讓它打勾或取消打勾。

cell & controller 使用 delegate 的技巧溝通

使用者點擊 cell 的圈圈按鈕時,將呼叫 cell 的 IBAction function。為了能通知 controller 使用者點擊了圈圈,範例利用 delegate 的技巧溝通,將 controller 設為 cell 的 delegate。

  • ToDoCell.swift

cell 的類別為 ToDoCell,在 ToDoCell.swift 可看到 protocol ToDoCellDelegate & 宣告為 ToDoCellDelegate 型別的 delegate。

protocol ToDoCellDelegate: AnyObject {
func checkmarkTapped(sender: ToDoCell)
}

class ToDoCell: UITableViewCell {

weak var delegate: ToDoCellDelegate?

使用者點擊 cell 的圈圈按鈕時,呼叫 delegate 的 checkmarkTapped,checkmarkTapped 的程式將在 controller 定義。

 @IBAction func completeButtonTapped(_ sender: UIButton) {

delegate?.checkmarkTapped(sender: self)
}
  • ToDoTableViewController.swift

view controller 的類別為 ToDoTableViewController。在 tableView(_:cellForRowAt:) 用 cell.delegate = self 將 view controller 設為 cell 的 delegate。

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ToDoCellIdentifier", for: indexPath) as! ToDoCell

let toDo = toDos[indexPath.row]
cell.titleLabel?.text = toDo.title
cell.isCompleteButton.isSelected = toDo.isComplete
cell.delegate = self

return cell
}

定義 protocol ToDoCellDelegate 的 function checkmarkTapped,使用者點擊 cell 的圈圈按鈕時將執行以下程式,實現以下功能。

  • 更新 Todo 項目資料的 array。
  • 更新表格畫面。
  • 儲存 Todo 項目資料的 array。
func checkmarkTapped(sender: ToDoCell) {
if let indexPath = tableView.indexPath(for: sender) {
var toDo = toDos[indexPath.row]
toDo.isComplete.toggle()
toDos[indexPath.row] = toDo
tableView.reloadRows(at: [indexPath], with: .automatic)
ToDo.saveToDos(toDos)
}
}

修改程式讓 delegate 造成 memory leak

Apple 原本的範例是完全正確的。為了說明 delegate 造成的 memory leak,我們進行以下修改,故意改成有問題的寫法。

  • ToDoCell.swift。

拿掉 delegate 的 weak。由於沒有 weak,protocol 冒號後的 AnyObject 也可以拿掉。

protocol ToDoCellDelegate {
func checkmarkTapped(sender: ToDoCell)
}

class ToDoCell: UITableViewCell {

var delegate: ToDoCellDelegate?
  • ToDoTableViewController.swift。

定義物件死掉時觸發的 deinit。待會我們將看到 delegate 造成 controller 無法死掉,因此不會觸發 deinit。

deinit {
print("ToDoTableViewController deinit")
}
  • Main.storyboard。

在 navigation controller 前加上 view controller。點選 view controller 的 button 將以 present modally 的方式顯示 ToDo 清單。

執行 App 見證記憶體問題

當我們點擊 button 切換到 ToDo 清單,然後再返回首頁時,將發現 Console 沒有印出 ToDoTableViewController deinit,因此問題果然大條了,ToDoTableViewController 沒有死掉,它還佔著寶貴的記憶體。

為什麼 delegate 會造成 memory leak

class ToDoCell: UITableViewCell {

var delegate: ToDoCellDelegate?

cell 的 delegate 為 ToDoTableViewController,當 delegate 沒有 weak 時,delegate 會增加 controller 的 reference count。而 controller 在 reference count 歸零時才會死掉,所以要等 cell 死掉,cell 的 property delegate 不再指到 controller 時,controller 的 reference count 才能歸零。

但是 cell 也無法死掉呀,因為 controller 沒死,controller 顯示的 table view & cell 都會繼續活著。

所以可怕的 memory leak 就發生了。雖然 ToDo 清單的畫面已經關掉了,controller,table view & cell 都還是活著,佔據著珍貴的記憶體。

利用 Debug Memory Graph 觀察 memory leak

剛剛的 memory leak,我們也可透過以下方法觀察。

點選 button 切換到 ToDo 清單,然後再返回首頁後,點選下圖的 Debug Memory Graph。

下圖可看到 ToDoTableViewController 還活著。

下圖顯示 cell 的 delegate 還指著 ToDoTableViewController。

將 delegate 宣告為 weak,解決 memory leak

解決 memory leak 的方法很簡單,只要將 delegate 宣告為 weak。

class ToDoCell: UITableViewCell {

weak var delegate: ToDoCellDelegate?

加了 weak 後會先出現以下錯誤。

weak 只能作用在 class 型別的資料,因此我們必須在 protocol 加上 AnyObject,限制只有 class 能遵從 protocol。

protocol ToDoCellDelegate: AnyObject {
func checkmarkTapped(sender: ToDoCell)
}

cell 的 delegate 宣告為 weak 後,它不會再增加 controller 的 reference count,因此當我們從 ToDo 清單返回首頁後,ToDoTableViewController 的 reference count 可以歸零,ToDoTableViewController 終於能順利死掉,Console 印出 ToDoTableViewController deinit

透過以下 cell 的 delegate 例子,我們明白了為何時常看到 delegate 宣告為 weak。因此若要避免 memory leak,最好將 delegate 宣告為 weak。

One more thing,有些情況不加 weak 的 delegate 也不會造成 memory leak

比方以下 App 將 EditEventTableViewController 設為 SelectFrequencyTableViewController 的 delegate,SelectFrequencyTableViewController 返回上一頁時以 delegate 通知 EditEventTableViewController 選擇的頻率。此時 delegate 不加 weak 也不會有記憶體問題,因為返回上一頁時 SelectFrequencyTableViewController 會死掉,它的 property delegate 不再指到 EditEventTableViewController,所以 EditEventTableViewController 的 reference count 可以正常地被扣除。

--

--

彼得潘的 iOS App Neverland
彼得潘的 Swift iOS App 開發問題解答集

彼得潘的iOS App程式設計入門,文組生的iOS App程式設計入門講師,彼得潘的 Swift 程式設計入門,App程式設計入門作者,http://apppeterpan.strikingly.com