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。要规避这样的结果,只能加强经验应用与养成良好编码习惯了。