Swift中的捕獲列表:強引用,弱引用,無主引用之間的區別
捕獲列表位于代碼中的閉包參數列表之前,并將環境中的值捕獲為強,弱或無主。我們經常使用它們,主要是為了避免循環引用
首先,看一下這個問題:
class Singer {
func playSong() {
print("Shake is off!")
}
}
創建一個方法,方法中實例化Singer
,并使用Singer實例的playSong()
創建一個閉包,并返回這個閉包以供其他地方使用
func sing() -> () -> Void {
let taylor = Singer()
let singing = {
taylor.playSong()
return
}
return singing
}
最后,我們可以調用sing()
來獲取我們可以在任何想要打印playSong()
的地方調用的函數
let singFunction = sing()
singFunction() //打印出 Shake is off!
強引用
除非你要求特別的東西,否則Swift會使用強大的捕獲功能。這意味著閉包將捕獲閉包內使用的任何外部值,并確保它們永遠不會被破壞
func sing() -> () -> Void {
let taylor = Singer()
let singing = {
taylor.playSong()
return
}
return singing
}
那個taylor
常量是在sing()
函數內部進行的,所以通常它會在函數結束時被銷毀。但是,它會在閉包內部使用,這意味著只要閉包存在某個地方,即使函數返回后,Swift也會自動確保它保持活動狀態。
這是一個強大的捕獲行動。如果Swift允許taylor
被摧毀,那么關閉將不再是安全的 - 它的taylor.playSong()
方法將不再有效
弱引用
Swift讓我們指定一個捕獲列表來確定如何捕獲閉包內使用的值。與強引用常見使用的是弱引用,它改變了兩件事:
1.閉包不能保持弱捕獲的值,因此它們可能會被破壞并設置為nil
。
2.作為1的結果,弱引用值在Swift中始終是可選的。這會阻止你假設它們存在,而實際上它們可能不存在。
func sing() -> () -> Void {
let taylor = Singer()
let singing = { [weak taylor] in
taylor?.playSong()
return
}
return singing
}
[weak taylor]
是我們的捕獲列表.它是閉包的一個特定部分,我們就如何捕獲值給出具體指示,在這里我們說taylor
應該被弱引用,這就是為什么我們需要使用taylor?.playSong()
- 它現在是可選的,因為它可以在任何時候設置為nil
方法修改以后,調用singFunction()
會發現不會打印任何東西,原因是taylor
只存在于sing()
中,因為它返回的閉包并沒有強持有它。
如果上面代碼修改成:
func sing() -> () -> Void {
let taylor = Singer()
let singing = { [weak taylor] in
taylor!.playSong()
return
}
return singing
}
會引發崩潰,因為taylor
是nil
無主引用
與weak
對應的是unowned
,其行為更像是隱式解包可選項。就像弱引用一樣,無主引用允許值在未來的任何時候都變為nil。但是,您可以使用它們,就像它們總是在那里一樣 --你不需要解包可選項
func sing() -> () -> Void {
let taylor = Singer()
let singing = { [unowned taylor] in
taylor.playSong()
return
}
return singing
}
這里會發生崩潰,與我們之前強制解包類似:unowned taylor
告訴我們taylor
會一直存在閉包的生命周期內,但實際上,taylor
幾乎立即被銷毀,所以會崩潰
常見問題
當使用閉包捕獲時,人們常常遇到四個問題:
- 當閉包接受參數時,他們不確定在何處使用捕獲列表。
- 它們會產生循環引用,導致內存被消耗。
- 他們無意中使用強引用,特別是在使用多個捕獲時。
- 他們復制閉包,并共享捕獲的數據。
讓我們通過簡單的代碼,看看都會發生什么
捕獲列表與參數在一起
當你第一次開始使用捕獲列表時,這是一個常見的問題,但幸運的是,Swift為我們做了規定。
當一起使用捕獲列表和閉包參數時,捕獲列表必須始終首先出現,然后單詞in
來標記閉包體的開始 - 嘗試將其放在閉包參數之后將阻止代碼編譯。
writeToLog { [weak self] user, message in
self?.addToLog("\(user) triggered event: \(message)")
}
循環引用
當A持有B,同時B持有A,稱之為循環引用
創建一個House
類,有一個屬性(閉包),一個方法,一個銷毀器,當銷毀時打印一條消息
class House {
var ownerDetails: (() -> Void)?
func printDetails() {
print("This is a great house.")
}
deinit {
print("I'm being demolished!")
}
}
這有一個類似的類Owner
,不同的是屬性(閉包)
class Owner {
var houseDetails: (() -> Void)?
func printDetails() {
print("I own a house.")
}
deinit {
print("I'm dying!")
}
}
我們在do
block中創建兩個類的示例,我們不需要catch
,確保在}
之前會銷毀
print("Creating a house and an owner")
do {
let house = House()
let owner = Owner()
}
print("Done")
//Creating a house and an owner
//I’m dying!
//I'm being demolished!
//Done
現在我們創建一個循環引用
print("Creating a house and an owner")
do {
let house = House()
let owner = Owner()
house.ownerDetails = owner.printDetails
owner.houseDetails = house.printDetails
}
print("Done")
//Creating a house and an owner
//Done
銷毀器中的信息不會被打印,這是因為 house
有一個屬性指向owner
,同時owner
有一個屬性指向house
,所以他們不能被安全銷毀,會導致內存不能釋放,稱之為內存泄露,會降低系統性能甚至應用終止
為解決這個問題,我們需要創建一個新的閉包,使用弱引用
print("Creating a house and an owner")
do {
let house = House()
let owner = Owner()
house.ownerDetails = { [weak owner] in owner?.printDetails() }
owner.houseDetails = { [weak house] in house?.printDetails() }
}
print("Done")
沒有必要將這兩個值都使用弱引用 - 重要的是至少有一個值,因為它允許Swift在必要時銷毀它們。
意外強引用
Swift默認為強引用,這可能會導致無意中的問題
func sing() -> () -> Void {
let taylor = Singer()
let adele = Singer()
let singing = { [unowned taylor, adele] in
taylor.playSong()
adele.playSong()
return
}
return singing
}
現在我們有兩個值被閉包捕獲,并且兩個值在閉包內使用方式相同,但是,只有taylor
被無主引用,--adele
被強引用,因為unowned
關鍵字被使用必須放在每個捕獲值前面
如果要兩個值都無主引用
[unowned taylor, unowned adele]
Swift確實提供了一些防止意外捕獲的措施,但是有限的:如果你在一個閉包中使用self
,Swift會強制你選擇使用self.
或者self?
讓你的意圖更清晰
在Swift中使用隱式self會發生很多事,比如,這個初始化程序調用playSong()
,真正意義是self.playSong()
---self
由上下文隱含
class Singer {
init() {
playSong()
}
func playSong() {
print("Shake it off!")
}
}
swift不會讓你在閉包內使用隱含self
,有助于減少循環引用
閉包復制
閉包會和它的復制閉包共享捕獲數據
var numberOfLinesLogged = 0
let logger1 = {
numberOfLinesLogged += 1
print("Lines logged: \(numberOfLinesLogged)")
}
logger1()
let logger2 = logger1
logger2()
logger1()
logger2()
//1
//2
//3
//4
什么時候使用強引用,什么時候使用弱引用,什么時候使用無主引用
- 如果你確定你的捕獲值永遠不會消失,而閉包有可能被調用,你可以使用
unowned
無主引用。這實際上只適用于weak
會導致使用煩惱的少數幾次,但可以在閉包內使用guard let
修飾弱引用變量 - 如果有循環引用的情況,那么兩個中的一個應該使用弱引用,通常是首先被銷毀的一個,所以如果視圖控制器A呈現視圖控制器B,則視圖控制器B可能將弱引用持有A。
- 如果不會發生循環引用,你可以使用強引用,例如執行動畫不會在動畫閉包內引起循環引用.所以你可以使用強引用