为什么我们应该避免在结构体使用闭包
我们所有人都喜欢闭包,你难道不喜欢吗? 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/