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。這個屬性引用了一個閉包,將name和text組合成 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類只提供一個構造函數,通過name和text(如果有的話)參數來初始化一個元素。該類也定義了一個析構函數,當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.name和self.text),因此閉包捕獲了self,這意味着閉包又反過來持有了HTMLElement實例的強引用。這樣兩個對象就產生了循環強引用。(更多關於閉包捕獲值的信息,請參考值捕獲)。
注意:
雖然閉包多次使用了self,它只捕獲HTMLElement實例的一個強引用。
如果設置paragraph變量爲nil,打破它持有的HTMLElement實例的強引用,HTMLElement實例和它的閉包都不會被銷燬,也是因爲循環強引用:
paragraph = nil
注意HTMLElementdeinitializer中的消息並沒有別打印,證明了HTMLElement實例並沒有被銷燬。