避免在Swift Struct中使用閉包

爲什麼我們應該避免在結構體使用閉包

我們所有人都喜歡閉包,你難道不喜歡嗎? Closure能夠讓iOS開發者生活更輕鬆。如果它讓我們更輕鬆了,那爲啥我還要說不在在結構體中使用閉包了,原因就是:“內存泄露和發生不可預料的事”,你會不會有爲啥Swift結構體會發生內存泄露問題的疑問?

Swift 結構體是值類型,按道理不會發生內存泄露,事實真的如此嗎?我們已經有很多疑問了,接下來我們看看Swift基本內存管理吧。

基本類型

在解決主要問題之前,我要強調基本類型。
Swfit 主要有兩個基本類型:引用類型和值類型,比如類就是引用類型,而結構體和枚舉都是值類型。

值類型

值類型數據直接存儲在內存,每個實例有唯一的複製數據,當變量賦值給存在的變量時,數據就會被複制。值類型的內存分配是在堆棧(stack)中完成。當值類型變量超出作用範圍,內存分配就會發生。

struct Person {
    var name : String
}
var oldPerson = Person(name: "Rizwan")
var newPerson = oldPerson
newPerson.name = "Oh my Swift"
print(oldPerson.name)
print(newPerson.name)

-------
Output:
Rizwan
Oh my Swift
-------

我們可以看到,newPerson變量的改變不會引起oldPerson的改變。這就是值類型的特性。

引用類型

引用類型在初始化時保留數據的引用(指針)。無論這個變量被分配到哪個已存在的引用類型上,這個引用共享變量值,引用類型的內存分配是在堆(heap)中完成.由自動引用計數(ARC)管理引用類型變量的內存。

class Person {
    var name: String
    init(withName name: String){
        self.name = name
    }
}
var oldPerson = Person(withName: "Rizwan")
var newPerson = oldPerson
newPerson.name = "Oh my Swift"
print(oldPerson.name)
print(newPerson.name)


------
Output
Oh my Swift
Oh my Swift
------ 

我們可以看到, olaPerson變量受newPerson的改變而改變,這就是引用類型的特性。

通常,內存泄露發生在引用類型。大多數情況發生在循環引用,想知道循環引用,可以閱讀這篇博客
如果引用類型引起循環引用,我們可以嘗試使用值類型去解決這個問題。

但是,這個也不是方法,有時候枚舉和結構體能作爲引用類型的參考,有時候循環引用也會在枚舉和結構體中發生。

閉包-是結構體中的反面教材

當你在閉包中使用結構體,就像在引用類型中使用閉包時,就會發生問題。閉包時擁有外部環境的引用,以便在執行閉包主體時修改引用外部環境的值。

有這樣一個例子,我們使用weak self來解決循環引用。如果我們嘗試在結構體中去這樣做,我們就會得到如下編譯錯誤:'weak self' many noly be applied to class add class-bound protocol type, not '{struct name}'(意思就是weak self只能用於類或者類協議)

struct Car {
    var speed: Float = 0.0
    var increaseSpeed: (() -> ())?
}
var myCar = Car()
myCar.increaseSpeed = {
    myCar.speed += 30 // The retain cycle occurs here. We cannot use [weak myCar] as myCar is a value type.
}
myCar.increaseSpeed?()
print("My car's speed :")
print(myCar.speed) // Prints 30

var myNewCar = myCar
myNewCar.increaseSpeed?()
myNewCar.increaseSpeed?()
print("My new car's speed :")
print(myNewCar.speed) // Prints 30 still! 

你大概期待myNewCar結果是90.0,但是數據爲30(而且引起循環引用了)

爲什麼

原因就是 myNewCar 只是 newCar 的部分複製,然而閉包和他的外部環境不能被完成複製。speed值被複制,但是myNewCar的屬性increaseSpeed持有的是myCar的increaseSpeed的環境,即myCar的speed環境。所有myCar的increaseSpeed被調用。

這就是爲什麼在swift中定義閉包使用是危險的。

那我們應該怎麼解決

最直接的辦法是避免在結構體內定義閉包使用。如果你想使用,你應該理解非常小心,可能會出現一些預料之外的問題。對於循環引用問題,唯一的方法是設置myCar和myNewCar爲nil,聽起來不好,但是這也是無賴之舉。

當我知道閉包在值類型中使用會有如此危險時,是如此令人深思。我希望你也有這種感受。

參考

[1] https://forums.swift.org/t/avoiding-unbreakable-reference-cycle-with-value-types-and-closures/18757/6

[2] https://github.com/Wolox/ios-style-guide/blob/master/rules/avoid-struct-closure-self.md

[3] https://www.objc.io/issues/16-swift/swift-classes-vs-structs/

[4] https://marcosantadev.com/capturing-values-swift-closures/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章