iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 15
0
自我挑戰組

iOS 新手開發的大小事系列 第 15

Day 15: 自動引用計數 (Automatic Reference Counting) -3

  • 分享至 

  • twitterImage
  •  

前言

前面兩天介紹了在類中引起的強引用循環,以及解決的方法,今天要來介紹的是閉包的強引用循環,其解決的方法如同類,只是在寫法上稍有不同,讓我們繼續看下去吧!


閉包的強引用循環 (Strong Reference Cycles for Closures)

我們在前面看到了當兩個類實例屬性相互之間具有強引用時,如何創造強引用循環,我們還了解瞭如何使用弱引用和無主引用來打破這些強引用循環。

如果為類實例的屬性指定為一個閉包,並且該閉包體捕獲實例,則也會發生強引用循環。這種捕獲可能是因為閉包的主體訪問實例的屬性,例如 self.someProperty,或者因為閉包調用實例上的方法,例如 self.someMethod()。在任何一種情況下,這些訪問都會導致閉包「捕獲」self,進而創建一個強引用循環。

這種強引用循環的發生是因為閉包(如類)是引用型別。將閉包指定給屬性時,我們將引用指定給該閉包。實質上,它與上面的問題相同 -- 兩個強引用互相保持彼此存活。但這次不是兩個類實例,而是一個類實例和一個閉包,它們互相保持彼此存活。

Swift 為這個問題提供了一個優雅的解決方案,稱為閉包捕獲列表。但在學習如何使用閉包捕獲列表打破強引用循環之前,該了解如何引起這樣的循環。

下面的範例顯示了在使用引用 self 的閉包時如何創建強引用循環。此範例定義了一個名為 HTMLElement 的類,它為 HTML 文檔中的單個元素提供了一個簡單模型:

class HTMLElement {

    let name: String
    let text: String?

    lazy var asHTML: () -> String = {
        if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        } else {
            return "<\(self.name) />"
        }
    }

    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }

    deinit {
        print("\(name) is being deinitialized")
    }

}

HTMLElement 類定義了一個屬性 name ,它指出元素的名稱,例如標題元素的 “h1”,段落元素的 “p” 或換行符元素的 “br”。 HTMLElement 還定義了一個屬性 text 可選型別,我們可以將其設置為表示要在該 HTML 元素中呈現的文本的字符串。

除了這兩個簡單屬性之外,類 HTMLElement 還定義了一個名為 asHTMLlazy 屬性。此屬性引用一個將 nametext 組合到 HTML 字符串片段中的閉包。asHTML 屬性的類型為 () -> String,或 “不帶參數的函數,並返回 String 值”。

預設情況下,為屬性 asHTML 分配一個閉包,該閉包返回 HTML 標記的字符串。此標記包含 text 可選型別(如果存在),如果 text 不存在則不包含文本內容。對於段落元素,閉包將返回 “\<p> some text \</p>”“\<p />”,具體取決於屬性 text 是否等於 “some text”nil

asHTML 屬性的命名和使用方式有點像實例方法。但因為 asHTML 是閉包屬性而不是實例方法,所以如果要更改特定 HTML 元素的 HTML 呈現,則可以使用自定義閉包替換 asHTML 屬性的預設值。

例如,如果屬性 textnil,則屬性 asHTML 可以設置為預設是某些文本的閉包,以防止返回空 HTML 標記:

let heading = HTMLElement(name: "h1")
let defaultText = "some default text"
heading.asHTML = {
    return "<\(heading.name)>\(heading.text ?? defaultText)</\(heading.name)>"
}
print(heading.asHTML())
// Prints "<h1>some default text</h1>"

屬性 asHTML 被宣告為一個 lazy 屬性,因為只有在元素實際需要呈現為某個 HTML 輸出目標的字符串值時才需要它。 asHTML 是一個 lazy 屬性的事實代表你可以在預設閉包中引用 self,因為在初始化完成且 self 存在之前,lazy 屬性不會被訪問。

HTMLElement 提供單個初始化程序,它需要一個參數 name 和(如果必須)一個參數 text 來初始化一個新元素。該類還定義了一個反初始化器,它印出一則訊息,用來顯示HTMLElement 實例何時被釋放。

以下是使用類 HTMLElement 創建並印出新實例的方法:

var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>"

上述變數 paragraph 被定義為可選的 HTMLElement,因此可以將其設置為 nil,以證明存在強引用循環。

不幸的是,如上所述,類 HTMLElement 在實例 HTMLElement 和用於其預設 asHTML 值的閉包之間創建了一個強引用循環。這是循環的樣子:

實例的屬性 asHTML 擁有對其閉包的強引用。但是,因為閉包在其體內引用 self(作為引用 self.nameself.text 的一種方式),閉包捕獲 self,這意味著它擁有一個強引用回到實例 HTMLElement。在兩者之間創建了強引用循環。 (有關在閉包中捕獲值的更多信息,請參閱捕獲值。)

即使閉包多次引用 self,它也只捕獲對實例 HTMLElement 的一個強引用。

如果將 paragraph 變數設置為 nil 並斷開其對實例 HTMLElement 的強引用,則由於強引用周期,HTMLElement實例及其閉包都不會被釋放:

paragraph = nil

請注意,不會印出 HTMLElement 反初始化器中的訊息,這代表實例HTMLElement 未被釋放。


解決閉包的強引用循環 (Resolving Strong Reference Cycles for Closures)

利用將捕獲列表定義為閉包定義的一部分,可以解決閉包和類實例之間的強引用循環。捕獲列表定義在閉包體內捕獲一個或多個引用型別時使用的規則。與兩個類實例之間的強引用循環一樣,您將每個捕獲的引用宣告為弱引用或無主引用,而不是強引用。弱或無主的適當選擇取決於代碼的不同部分之間的關係。

每當在一個閉包中引用 self 的成員時,Swift 都要求你編寫 self.somePropertyself.someMethod()(而不僅僅是 somePropertysomeMethod())。這有助於您記住,是有可能偶然捕獲自己的。

定義捕獲列表 (Defining a Capture List)

捕獲列表中的每項都是弱或無主關鍵字和類實例(如 self)的引用或使用某個值初始化的變數(如 delegate = self.delegate!)的配對。這些配對寫在一對方括號內,用逗號分隔。

將捕獲列表放在閉包的參數列表之前,如果提供了它們,則返回型別:

lazy var someClosure: (Int, String) -> String = {
    [unowned self, weak delegate = self.delegate!] (index: Int, stringToProcess: String) -> String in
    // closure body goes here
}

如果閉包沒有指定參數列表或返回型別,因為它們可以從上下文中推斷出來,請將捕獲列表放在閉包的最開頭,然後是 in 關鍵字:

lazy var someClosure: () -> String = {
    [unowned self, weak delegate = self.delegate!] in
    // closure body goes here
}

弱引用和無主引用 (Weak and Unowned References)

當閉包和它捕獲的實例始終相互引用時,將閉包中的捕獲定義為無主引用,並且同時被釋放。

相反的,當捕獲的引用在將來的某個時刻變為 nil 時,將捕獲定義為弱引用。弱引用始終是可選型別,並且在它們引用的實例被釋放時自動變為 nil。這使我們可以檢查它們在閉包體內是否存在。

如果捕獲的引用永遠不會變為 nil,則應始終將其捕獲為無主引用,而不是弱引用。

無主引用是用於解決 HTMLElement 範例中的強引用循環的適當捕獲方法,該方法來自之前所述的閉包的強引用循環。以下是編寫類 HTMLElement 以避免循環的方法:

class HTMLElement {

    let name: String
    let text: String?

    lazy var asHTML: () -> String = {
        [unowned self] in
        if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        } else {
            return "<\(self.name) />"
        }
    }

    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }

    deinit {
        print("\(name) is being deinitialized")
    }

}

除了在 asHTML 閉包中添加捕獲列表之外,HTMLElement 的這種實現與先前的實現相同。在這種情況下,捕獲列表是 [unowned self],代表「將自己捕獲為無主引用而非強引用」。

可以像以前一樣創建和印出實例 HTMLElement

var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>"

以下是引用在捕獲列表中的實際情形:

這一次,閉包捕獲 self 是一個無主引用,並沒有對它捕獲的實例 HTMLElement 擁有強力的保持。如果將 paragraph 變數中的強引用設置為 nil,則會釋放實例 HTMLElement,這可以從下面範例中的反初始化器印出反初始化的訊息:

paragraph = nil
// Prints "p is being deinitialized"

上一篇
Day 14: 自動引用計數 (Automatic Reference Counting) -2
下一篇
Day 16: 協定 (Protocol) -1
系列文
iOS 新手開發的大小事30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言