Swift lazy 属性的本质
我们经常出于性能的考虑,会在 Swift 开发中使用 lazy
属性或变量,它是一个语法糖,帮助我们只在必要时(初次访问)才完成初始化,构造出实例。
比如:
class MyExpensiveObject {
// ...
func foo() {
}
}
class ViewController: UIViewController {
lazy var object = MyExpensiveObject()
override viewDidLoad() {
super.viewDidLoad()
self.object.foo()
}
}
那么它在运行时究竟干了什么呢?怎么做到初次访问时才构造的?
上述例子在运行时点断点,不难看出,self
有个成员变量名为:$__lazy_storage_$_object
,而且它是可选类型:MyExpensiveObject?
。这就不难理解,其实对于所有的 lazy
属性 var: Type
,编译器会在编译时生成一个对应的 $__lazy_storage_$_var: Type?
。结合我们之前在 Objective-C 里手动实现懒加载的写法,可以推断出以下等价写法:
class ViewController: UIViewController {
lazy var object = MyExpensiveObject()
}
// 等价于
class ViewController: UIViewController {
private var $__lazy_storage_$_object: MyExpensiveObject?
var object: MyExpensiveObject {
get {
if ($__lazy_storage_$_object == nil) {
$__lazy_storage_$_object = MyExpensiveObject()
}
return $__lazy_storage_$_object!
}
set {
$__lazy_storage_$_object = newValue
}
}
}
那么,问题来了,lazy
属性一定只会初始化一次吗?
其实是无法保证的,在某些不合适的编码习惯下,它可能初始化多次。比如下面这个比较经典的例子:
class MyView: UIView {
}
class MyViewController: UIViewController {
lazy var myView = MyView(frame: self.view.bounds)
init() {
super.init()
self.myView.backgroundColor = .red
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .green
view.addSubview(self.myView)
}
}
先思考一下,最终呈现出来的是红色,还是绿色?
答案是:绿色。因为,这里 MyView 构造出了两个实例。
首先,在 self.myView.backgroundColor = .red
执行前,它要构造 MyView 实例,而它依赖 self.view.bounds
,访问 self.view
会触发 view
的提前加载,导致提前进入 viewDidLoad
方法。
进入 viewDidLoad
方法后,view.addSubview(self.myView)
又再次访问 self.myView
,于是需要构造 MyView 实例 1,构造实例 1 时,访问 self.view.bounds
已经不是问题了,所以它成功构造,并且加为子视图,没有背景色。
然后 viewDidLoad
方法退出,回到 init
方法里,此时 MyView 实例 2 也成功构造,并给它设置背景为红色,但它没有加为子视图,因为 viewDidLoad
已经调过一次了。
这个非预期的结果其实跟在 dispatch_once 里又访问自己,导致需要再次 dispatch_once 类似,只不过 dispatch_once 构造两次时会直接 crash。要规避这样的结果,只能加强经验应用与养成良好编码习惯了。