我們都喜歡閉包,不是嗎?
閉包可以簡化iOS開發(fā)人員的工作。好吧,如果這使我們工作變得容易,那為什么我要避免在Swift結(jié)構(gòu)體中使用閉包呢?
原因是:內(nèi)存泄漏和意外行為。
結(jié)構(gòu)體內(nèi)存泄漏,可能嗎?
結(jié)構(gòu)體是值類型,并且不可能發(fā)生內(nèi)存泄漏。這句話是真的嗎?我們已經(jīng)有很多問題了。因此,讓我們回顧一下Swift中的內(nèi)存管理基礎(chǔ)知識。
Swift中的基本類型分為兩類。一種是“引用類型(Reference type)”,另一種是“值類型(Value type)”。通常,類是引用類型。另一方面,結(jié)構(gòu)體和枚舉是值類型。
值類型(Value type)
值類型將數(shù)據(jù)直接存儲在內(nèi)存中。每個實例都有唯一的數(shù)據(jù)副本。將變量分配給現(xiàn)有變量后,將復(fù)制數(shù)據(jù)。值類型的分配在堆棧中完成。當(dāng)值類型變量超出范圍時,將發(fā)生內(nèi)存的重新分配。
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)
引用類型在初始化時保留對數(shù)據(jù)的引用(即指針)。只要將變量分配給現(xiàn)有引用類型,該引用就在變量之間共享。引用類型的分配在堆中完成。ARC(自動引用計數(shù))處理引用類型變量的取消分配。
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變量中的更改。這就是引用類型的工作方式。通常,在引用類型中會發(fā)生內(nèi)存泄漏。在大多數(shù)情況下,它以循環(huán)引用(retain cycles)的形式出現(xiàn)。
因此,如果引用類型是導(dǎo)致內(nèi)存泄漏的原因,那么我們可以將值類型用于所有情況。那就應(yīng)該解決問題。
不幸的是,這種情況并非如此。有時,結(jié)構(gòu)體和枚舉可以被視為引用類型,這意味著循環(huán)引用(retain cycles)也可以在結(jié)構(gòu)體和枚舉中發(fā)生。
結(jié)構(gòu)體中產(chǎn)生循環(huán)引用的罪魁禍首——閉包(Closures)
當(dāng)您在結(jié)構(gòu)中使用閉包時,閉包的行為就像一個引用類型,問題就從那里開始。閉包需要引用外部環(huán)境,以便在執(zhí)行閉包主體時可以修改外部變量。
在使用類(Class)的情況下,我們可以使用[weak self]打破循環(huán)引用。當(dāng)我們嘗試對某個結(jié)構(gòu)體執(zhí)行此操作時,會出現(xiàn)以下編譯器錯誤,'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)")
大膽猜測一下最終打印的結(jié)果

我想你開始想的是3和4最終打印的速度值都是——60,但是結(jié)果可能有點不一樣:
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!
原因解析:
結(jié)構(gòu)體myNewCar是結(jié)構(gòu)體myCar的部分副本。由于閉包及其環(huán)境無法完全復(fù)制,屬性speed的值被復(fù)制了,但是myNewCar的屬性increaseSpeed在捕獲的環(huán)境變量中引用了myCar的increaseSpeed和myCar的speed。因此,myNewCar.increaseSpeed?()最終調(diào)用的是myCar的increaseSpeed,所以最終打印的值就是myCar的值變成了90。
這就是為什么Swift結(jié)構(gòu)中的閉包很危險的原因。
直接的解決方案是,避免在值類型中使用閉包。如果要使用它們,則應(yīng)格外小心,否則可能會導(dǎo)致意外結(jié)果。關(guān)于保留周期,打破它們的唯一方法是將變量myCar和myNewCar手動設(shè)置為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/
賞我一個贊吧~~~