前面兩天介紹了在類中引起的強引用循環,以及解決的方法,今天要來介紹的是閉包的強引用循環,其解決的方法如同類,只是在寫法上稍有不同,讓我們繼續看下去吧!
我們在前面看到了當兩個類實例屬性相互之間具有強引用時,如何創造強引用循環,我們還了解瞭如何使用弱引用和無主引用來打破這些強引用循環。
如果為類實例的屬性指定為一個閉包,並且該閉包體捕獲實例,則也會發生強引用循環。這種捕獲可能是因為閉包的主體訪問實例的屬性,例如 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
還定義了一個名為 asHTML
的 lazy
屬性。此屬性引用一個將 name
和 text
組合到 HTML 字符串片段中的閉包。asHTML
屬性的類型為 () -> String
,或 “不帶參數的函數,並返回 String 值”。
預設情況下,為屬性 asHTML
分配一個閉包,該閉包返回 HTML 標記的字符串。此標記包含 text
可選型別(如果存在),如果 text
不存在則不包含文本內容。對於段落元素,閉包將返回 “\<p> some text \</p>”
或 “\<p />”
,具體取決於屬性 text
是否等於 “some text”
或 nil
。
asHTML
屬性的命名和使用方式有點像實例方法。但因為 asHTML
是閉包屬性而不是實例方法,所以如果要更改特定 HTML 元素的 HTML 呈現,則可以使用自定義閉包替換 asHTML
屬性的預設值。
例如,如果屬性 text
為 nil
,則屬性 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.name
和 self.text
的一種方式),閉包捕獲 self
,這意味著它擁有一個強引用回到實例 HTMLElement
。在兩者之間創建了強引用循環。 (有關在閉包中捕獲值的更多信息,請參閱捕獲值。)
即使閉包多次引用
self
,它也只捕獲對實例HTMLElement
的一個強引用。
如果將 paragraph
變數設置為 nil
並斷開其對實例 HTMLElement
的強引用,則由於強引用周期,HTMLElement實例及其閉包都不會被釋放:
paragraph = nil
請注意,不會印出 HTMLElement
反初始化器中的訊息,這代表實例HTMLElement
未被釋放。
利用將捕獲列表定義為閉包定義的一部分,可以解決閉包和類實例之間的強引用循環。捕獲列表定義在閉包體內捕獲一個或多個引用型別時使用的規則。與兩個類實例之間的強引用循環一樣,您將每個捕獲的引用宣告為弱引用或無主引用,而不是強引用。弱或無主的適當選擇取決於代碼的不同部分之間的關係。
每當在一個閉包中引用
self
的成員時,Swift 都要求你編寫self.someProperty
或self.someMethod()
(而不僅僅是someProperty
或someMethod()
)。這有助於您記住,是有可能偶然捕獲自己的。
捕獲列表中的每項都是弱或無主關鍵字和類實例(如 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
}
當閉包和它捕獲的實例始終相互引用時,將閉包中的捕獲定義為無主引用,並且同時被釋放。
相反的,當捕獲的引用在將來的某個時刻變為 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"