我們都喜歡閉包,不是嗎?
閉包可以簡化iOS開發人員的工作。好吧,如果這使我們工作變得容易,那為什么我要避免在Swift結構體中使用閉包呢?
原因是:內存泄漏和意外行為。
結構體內存泄漏,可能嗎?
結構體是值類型,并且不可能發生內存泄漏。這句話是真的嗎?我們已經有很多問題了。因此,讓我們回顧一下Swift中的內存管理基礎知識。
Swift中的基本類型分為兩類。一種是“引用類型(Reference type)”,另一種是“值類型(Value type)”。通常,類是引用類型。另一方面,結構體和枚舉是值類型。
值類型(Value type)
值類型將數據直接存儲在內存中。每個實例都有唯一的數據副本。將變量分配給現有變量后,將復制數據。值類型的分配在堆棧中完成。當值類型變量超出范圍時,將發生內存的重新分配。
struct Person {
var name : String
}
var oldPerson = Person(name: "韋弦zhy")
var newPerson = oldPerson
newPerson.name = "Swift Struct"
print(oldPerson.name)
print(newPerson.name)
-------
Output:
韋弦zhy
Swift Struct
-------
我們可以看到,更改newPerson
的值不會更改oldPerson
的值。這就是值類型的工作方式。
引用類型(Reference type)
引用類型在初始化時保留對數據的引用(即指針)。只要將變量分配給現有引用類型,該引用就在變量之間共享。引用類型的分配在堆中完成。ARC(自動引用計數)處理引用類型變量的取消分配。
class Person {
var name: String
init(withName name: String){
self.name = name
}
}
var oldPerson = Person(withName: "韋弦zhy")
var newPerson = oldPerson
newPerson.name = "Swift Struct"
print(oldPerson.name)
print(newPerson.name)
------
Output
Swift Struct
Swift Struct
------
我們可以看到更改oldPerson
變量反映了newPerson
變量中的更改。這就是引用類型的工作方式。通常,在引用類型中會發生內存泄漏。在大多數情況下,它以循環引用(retain cycles)的形式出現。
因此,如果引用類型是導致內存泄漏的原因,那么我們可以將值類型用于所有情況。那就應該解決問題。
不幸的是,這種情況并非如此。有時,結構體和枚舉可以被視為引用類型,這意味著循環引用(retain cycles)也可以在結構體和枚舉中發生。
結構體中產生循環引用的罪魁禍首——閉包(Closures)
當您在結構中使用閉包時,閉包的行為就像一個引用類型,問題就從那里開始。閉包需要引用外部環境,以便在執行閉包主體時可以修改外部變量。
在使用類(Class
)的情況下,我們可以使用[weak self]
打破循環引用。當我們嘗試對某個結構體執行此操作時,會出現以下編譯器錯誤,'weak' may only be applied to class and class-bound protocol types, not 'struct name'
,比如如下代碼:
struct Car {
var speed: Float = 0.0
var increaseSpeed: (() -> ())?
}
var myCar = Car()
myCar.increaseSpeed = { //[weak myCar] in
myCar.speed += 30
// The retain cycle occurs here. We cannot use [weak myCar] as myCar is a value type.
//'weak' may only be applied to class and class-bound protocol types, not 'Car'
}
myCar.increaseSpeed?()
print("1: My car's speed \n\(myCar.speed)")
var myNewCar = myCar
print("2: My new car's speed \n\(myNewCar.speed)")
myNewCar.increaseSpeed?()
print("3: My new car's speed \n\(myNewCar.speed)")
myCar.increaseSpeed?()
print("4: My car's speed \n\(myCar.speed)")
大膽猜測一下最終打印的結果
我想你開始想的是3和4最終打印的速度值都是——60,但是結果可能有點不一樣:
1: My car's speed
30.0
2: My new car's speed
30.0
3: My new car's speed
30.0
4: My car's speed
90.0
是的,是90!
原因解析:
結構體myNewCar
是結構體myCar
的部分副本。由于閉包及其環境無法完全復制,屬性speed
的值被復制了,但是myNewCar
的屬性increaseSpeed
在捕獲的環境變量中引用了myCar
的increaseSpeed
和myCar
的speed
。因此,myNewCar.increaseSpeed?()
最終調用的是myCar
的increaseSpeed
,所以最終打印的值就是myCar
的值變成了90。
這就是為什么Swift結構中的閉包很危險的原因。
直接的解決方案是,避免在值類型中使用閉包。如果要使用它們,則應格外小心,否則可能會導致意外結果。關于保留周期,打破它們的唯一方法是將變量myCar
和myNewCar
手動設置為nil。聽起來并不理想,但是沒有其他方法。
參考:
[1] https://ohmyswift.com/blog/2020/01/10/why-should-we-avoid-using-closures-in-swift-structs/
[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/
賞我一個贊吧~~~