位置:首頁 > 高級語言 > Swift教學 > Swift閉包引起的循環強引用

Swift閉包引起的循環強引用

閉包引起的循環強引用

前麵我們看到了循環強引用環是在兩個類實例屬性互相保持對方的強引用時產生的,還知道了如何用弱引用和無主引用來打破循環強引用。

循環強引用還會發生在當你將一個閉包賦值給類實例的某個屬性,並且這個閉包體中又使用了實例。這個閉包體中可能訪問了實例的某個屬性,例如self.someProperty,或者閉包中調用了實例的某個方法,例如self.someMethod。這兩種情況都導致了閉包 “捕獲" self,從而產生了循環強引用。

循環強引用的產生,是因為閉包和類相似,都是引用類型。當你把一個閉包賦值給某個屬性時,你也把一個引用賦值給了這個閉包。實質上,這跟之前的問題是一樣的-兩個強引用讓彼此一直有效。但是,和兩個類實例不同,這次一個是類實例,另一個是閉包。

Swift 提供了一種優雅的方法來解決這個問題,稱之為閉包占用列表(closuer capture list)。同樣的,在學習如何用閉包占用列表破壞循環強引用之前,先來了解一下循環強引用是如何產生的,這對我們是很有幫助的。

下麵的例子為你展示了當一個閉包引用了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 {
        println("\(name) is being deinitialized")
    }

}

HTMLElement類定義了一個name屬性來表示這個元素的名稱,例如代表段落的"p",或者代表換行的"br"。HTMLElement還定義了一個可選屬性text,用來設置和展現 HTML 元素的文本。

除了上麵的兩個屬性,HTMLElement還定義了一個lazy屬性asHTML。這個屬性引用了一個閉包,將nametext組合成 HTML 字符串片段。該屬性是() -> String類型,或者可以理解為“一個冇有參數,返回String的函數”。

默認情況下,閉包賦值給了asHTML屬性,這個閉包返回一個代表 HTML 標簽的字符串。如果text值存在,該標簽就包含可選值text;如果text不存在,該標簽就不包含文本。對於段落元素,根據text是"some text"還是nil,閉包會返回"<p>some text</p>"或者"<p />"。

可以像實例方法那樣去命名、使用asHTML屬性。然而,由於asHTML是閉包而不是實例方法,如果你想改變特定元素的 HTML 處理的話,可以用自定義的閉包來取代默認值。


注意:
asHTML聲明為lazy屬性,因為隻有當元素確實需要處理為HTML輸出的字符串時,才需要使用asHTML。也就是說,在默認的閉包中可以使用self,因為隻有當初始化完成以及self確實存在後,才能訪問lazy屬性。
 

HTMLElement類隻提供一個構造函數,通過nametext(如果有的話)參數來初始化一個元素。該類也定義了一個析構函數,當HTMLElement實例被銷毀時,打印一條消息。

下麵的代碼展示了如何用HTMLElement類創建實例並打印消息。

var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
println(paragraph!.asHTML())
// prints"hello, world"


注意:
上麵的paragraph變量定義為可選HTMLElement,因此我們可以賦值nil給它來演示循環強引用。
 

不幸的是,上麵寫的HTMLElement類產生了類實例和asHTML默認值的閉包之間的循環強引用。循環強引用如下圖所示:

實例的asHTML屬性持有閉包的強引用。但是,閉包在其閉包體內使用了self(引用了self.nameself.text),因此閉包捕獲了self,這意味著閉包又反過來持有了HTMLElement實例的強引用。這樣兩個對象就產生了循環強引用。(更多關於閉包捕獲值的信息,請參考值捕獲)。


注意:
雖然閉包多次使用了self,它隻捕獲HTMLElement實例的一個強引用。
 

如果設置paragraph變量為nil,打破它持有的HTMLElement實例的強引用,HTMLElement實例和它的閉包都不會被銷毀,也是因為循環強引用:

paragraph = nil

注意HTMLElementdeinitializer中的消息並冇有彆打印,證明了HTMLElement實例並冇有被銷毀。