Date | Notes | Info |
---|---|---|
2020-09-18 | 纳入 Swift 拾遗系列,并重新整理完善 | Swift 5.3, Xcode 12.0 |
2017-04-27 | 扩充「延迟存储属性」部分并新增「devxoul/Then」一节 | Swift 3.1, Xcode 8.3.2 |
2016-10-26 | 首次提交 | Swift 3.0, Xcode 8.1 Beta 3 |
Preface
《Swift 拾遗》是一个关于 Swift 的新文章专辑,这个系列的文章将不会涉及基本语法的层面,而是尝试从底层「拾」起之前所忽视的内容。今天我们将一起简单探究 Swift 中的属性。
存储属性与计算属性
struct Foo {
var bar: Int
var baz: Int {
get {
bar * 2
}
set {
bar = newValue / 2
}
}
var customSetParamBaz: Int {
get {
bar * 2
}
set(customNewValue) {
bar = customNewValue / 2
}
}
}
var foo = Foo(bar: 10)
_ = foo.bar // 10
_ = foo.baz // 20 // BREAKPOINT 🔴
foo.baz = 100
_ = foo.baz // 100
_ = foo.bar // 50
如上,bar
为存储属性,即将占用实例对象的内存空间;而 baz
为计算属性,将通过其它变量得出,当外界取值时将得到调用 get
中的返回,设置时将通过 set
中 newValue
将外界的值传递进入并进行一定的计算。那么计算属性是否会占用实例对象的内存空间呢?
print(MemoryLayout<Foo>.size) // 8
print(MemoryLayout<Foo>.stride) // 8
我们也可以运行代码,并在断点处以汇编代码分析:
-> 0x1000027a1 <+113>: movq %rax, %rdi
0x1000027a4 <+116>: leaq -0x38(%rbp), %rsi
0x1000027a8 <+120>: movl $0x20, %edx
0x1000027ad <+125>: callq 0x100003b92 ; symbol stub for: swift_beginAccess
0x1000027b2 <+130>: movq 0x59bf(%rip), %rdi ; demo.foo : demo.Foo
; 将调用计算属性的 getter
0x1000027b9 <+137>: callq 0x1000028c0 ; demo.Foo.baz.getter : Swift.Int at main.swift:6
0x1000027be <+142>: leaq -0x38(%rbp), %rdi
0x1000027c2 <+146>: movq %rax, -0xa0(%rbp)
0x1000027c9 <+153>: callq 0x100003b98 ; symbol stub for: swift_endAccess
0x1000027ce <+158>: leaq 0x59a3(%rip), %rax ; demo.foo : demo.Foo
0x1000027d5 <+165>: xorl %r8d, %r8d
0x1000027d8 <+168>: movl %r8d, %ecx
0x1000027db <+171>: movq %rax, %rdi
0x1000027de <+174>: leaq -0x50(%rbp), %rsi
0x1000027e2 <+178>: movl $0x21, %edx
0x1000027e7 <+183>: callq 0x100003b92 ; symbol stub for: swift_beginAccess
0x1000027ec <+188>: movl $0x64, %edi
0x1000027f1 <+193>: leaq 0x5980(%rip), %r13 ; demo.foo : demo.Foo
; 将调用计算属性的 setter
0x1000027f8 <+200>: callq 0x100002960 ; demo.Foo.baz.setter : Swift.Int at main.swift:10
0x1000027fd <+205>: leaq -0x50(%rbp), %rdi
0x100002801 <+209>: callq 0x100003b98 ; symbol stub for: swift_endAccess
如上,计算属性将不会产生实际的变量,外界的使用将通过 get
和 set
中所生成的 getter
和 setter
方法访问或设置,因此也就不会占用实例对象的内存空间。
计算属性也支持只读(Read-only),只需要不实现 set
即可,此时也可省略显式的 get
关键字:
class Foo {
var bar: Int = 0
var readonlyBaz1: Int {
get {
bar * 2
}
}
var readonlyBaz2: Int {
bar * 2
}
}
var foo = Foo()
// ERROR: Cannot assign to property: 'readonlyBaz1' is a get-only property
foo.readonlyBaz1 = 10
// ERROR: Cannot assign to property: 'readonlyBaz2' is a get-only property
foo.readonlyBaz2 = 10
属性(存储属性与计算属性)也支持为 get
和 set
设置不同的访问控制级别:
class Foo {
var bar: Int = 0
internal private(set) var privateSetBaz3: Int {
get {
bar * 2
}
set {
bar = newValue / 2
}
}
private(set) var privateSetBaz4: Int {
get {
bar * 2
}
set {
bar = newValue / 2
}
}
func innerFunc() {
self.privateSetBaz3 = 10
self.privateSetBaz4 = 10
}
}
var foo = Foo()
foo.innerFunc()
// ERROR: Cannot assign to property: 'privateSetBaz3' setter is inaccessible
foo.privateSetBaz3 = 10
// ERROR: Cannot assign to property: 'privateSetBaz4' setter is inaccessible
foo.privateSetBaz4 = 10
那么计算属性有何使用场景呢?其实当定义的属性依赖于其它属性时,就可以使用计算属性。枚举中的原始值 rawValue
就是通过只读计算属性实现的:
enum Foo: Int {
case bar = 0, baz
}
_ = Foo.bar.rawValue // 0 // BREAKPOINT 🔴
尝试运行后在断点处查看汇编:
demo`main:
0x100002500 <+0>: pushq %rbp
0x100002501 <+1>: movq %rsp, %rbp
0x100002504 <+4>: subq $0x20, %rsp
0x100002508 <+8>: xorl %eax, %eax
0x10000250a <+10>: movl %edi, -0x4(%rbp)
-> 0x10000250d <+13>: movl %eax, %edi
0x10000250f <+15>: movq %rsi, -0x10(%rbp)
; 将调用计算属性的 getter
0x100002513 <+19>: callq 0x1000025a0 ; demo.Foo.rawValue.getter : Swift.Int at <compiler-generated>
0x100002518 <+24>: xorl %ecx, %ecx
0x10000251a <+26>: movq %rax, -0x18(%rbp)
0x10000251e <+30>: movl %ecx, %eax
0x100002520 <+32>: addq $0x20, %rsp
0x100002524 <+36>: popq %rbp
0x100002525 <+37>: retq
延迟存储属性
延迟存储属性是指该属性的初始化过程将延迟到首次使用时,而非跟随实例的创建而创建。当属性未被使用时,将避免消耗不必要的性能。延迟存储属性使用 lazy
关键字修饰:
struct Foo {
lazy var bar: Int = {
print("bar will init")
return 1
}()
var baz: Int = {
print("baz will init")
return 1
}()
}
_ = Foo()
// baz will init
如上,当构造 Foo
实例对象时,延迟存储属性将暂时不初始化。另外,延迟存储属性需要有几个注意点:首先,延迟存储属性必须使用 var
修饰,而不可使用 let
修饰。这是因为 let
常量一旦创建后就不能再改变,而延迟存储属性会在使用时再初始化将与此相悖;其次,结构体类型中若存在延迟存储属性,那么使用 let
声明的结构体常量将无法访问延迟存储属性,原因其实同理,在首次访问时将执行延迟存储属性的初始化,将会导致常量内存空间改变,也与常量本身的概念相悖;但 class
类型的变量由于本质是引用类型,即使堆空间的内存空间改变,也不会导致指针指向的内存空间地址改变,因此 class
类型的常量仍可以访问延迟存储属性:
class FooClass {
lazy var a = 1
}
struct FooStruct {
lazy var a = 1
}
let fc = FooClass()
print(fc.a)
let fs = FooStruct()
// ERROR: Cannot use mutating getter on immutable value: 'fs' is a 'let' constant
print(fs.a)
还需要注意的是,延迟存储属性是线程不安全的,即有可能在多线程中被多次初始化:
import Foundation
struct Foo {
lazy var bar: Int = {
print("bar will init")
return 1
}()
var baz: Int = {
print("baz will init")
return 1
}()
}
var foo = Foo()
var array = [Int]()
DispatchQueue.concurrentPerform(iterations: 3) { _ in
print("--- \(Thread.current) ---")
print(foo.bar)
}
// baz will init
// --- <NSThread: 0x100611a40>{number = 3, name = (null)} ---
// --- <NSThread: 0x1029197f0>{number = 2, name = (null)} ---
// --- <NSThread: 0x10050c580>{number = 1, name = main} ---
// bar will init
// bar will init
// 1
// 1
// bar will init
// 1
如上,我们使用 GCD 的并发执行 API concurrentPerform
执行三次,如果延迟存储属性是线程安全的,那么将只会进入其构造方法一次,但此时其实输出了三句 bar will init
。
lazy
方法let doubled = (0..<3).map { i -> Int in print("mapping") return i * 2 } let lazyDoubled = (0..<3).lazy.map { i -> Int in print("lazy mapping") return i * 2 } // mapping // mapping // mapping lazyDoubled.forEach { print($0) } // lazy mapping // 0 // lazy mapping // 2 // lazy mapping // 4
在 Swift 标准库中也有一些内置的
lazy
方法,它们的执行时机也将推迟到实际使用该方法返回值的时刻。
属性观察器
在 Swift 中,我们还可以对非延迟存储属性添加属性观察器,以便在值发生改变之前和之后进行一些操作:
struct Foo {
var bar: Int {
willSet {
print("\(bar) will set to \(newValue)")
}
didSet {
// oldValue 会在 willSet 进行保存
print("\(oldValue) did set to \(bar)")
}
}
var customValueParamsBar: Int = 0 {
willSet(new) {
print("\(customValueParamsBar) will set \(new)")
}
didSet(old) {
print("\(old) did set to \(customValueParamsBar)")
}
}
}
var foo = Foo(bar: 10)
foo.bar = 20 // BREAKPOINT 🔴
// 10 will set to 20
// 10 did set to 20
如上,在值被真正改变之前,将先通过 willSet
方法,新值将默认可以通过 newValue
变量访问,此时因为尚未执行赋值,因此旧值即可通过变量本身 bar
访问;设置后同理可通过 didSet
方法,此时旧值将默认可以通过 oldValue
变量访问,新值则可直接通过变量本身 bar
访问。
尝试运行后在断点处查看汇编:
-> 0x100001c2e <+62>: movq %rcx, %rdi
0x100001c31 <+65>: leaq -0x20(%rbp), %rax
0x100001c35 <+69>: movq %rsi, -0x38(%rbp)
0x100001c39 <+73>: movq %rax, %rsi
0x100001c3c <+76>: movl $0x21, %edx
0x100001c41 <+81>: movq -0x38(%rbp), %rcx
0x100001c45 <+85>: callq 0x100003bc0 ; symbol stub for: swift_beginAccess
0x100001c4a <+90>: movl $0x14, %edi
0x100001c4f <+95>: leaq 0x655a(%rip), %r13 ; demo.foo : demo.Foo
; 将调用 setter
0x100001c56 <+102>: callq 0x100001cb0 ; demo.Foo.bar.setter : Swift.Int at main.swift:4
0x100001c5b <+107>: leaq -0x20(%rbp), %rdi
0x100001c5f <+111>: callq 0x100003bd2 ; symbol stub for: swift_endAccess
demo`Foo.bar.setter:
-> 0x100001cb0 <+0>: pushq %rbp
0x100001cb1 <+1>: movq %rsp, %rbp
0x100001cb4 <+4>: pushq %r13
0x100001cb6 <+6>: subq $0x38, %rsp
0x100001cba <+10>: movq $0x0, -0x10(%rbp)
0x100001cc2 <+18>: movq $0x0, -0x18(%rbp)
0x100001cca <+26>: movq $0x0, -0x20(%rbp)
0x100001cd2 <+34>: movq %rdi, -0x10(%rbp)
0x100001cd6 <+38>: movq %r13, -0x18(%rbp)
0x100001cda <+42>: movq (%r13), %rax
0x100001cde <+46>: movq %rax, -0x20(%rbp)
0x100001ce2 <+50>: movq %rdi, -0x28(%rbp)
0x100001ce6 <+54>: movq %r13, -0x30(%rbp)
0x100001cea <+58>: movq %rax, -0x38(%rbp)
; 将调用 willset
0x100001cee <+62>: callq 0x100001d20 ; demo.Foo.bar.willset : Swift.Int at main.swift:5
0x100001cf3 <+67>: movq -0x30(%rbp), %rax
0x100001cf7 <+71>: movq -0x28(%rbp), %rcx
0x100001cfb <+75>: movq %rcx, (%rax)
0x100001cfe <+78>: movq -0x38(%rbp), %rdi
0x100001d02 <+82>: movq %rax, %r13
; 将调用 didset
0x100001d05 <+85>: callq 0x100001fd0 ; demo.Foo.bar.didset : Swift.Int at main.swift:9
0x100001d0a <+90>: addq $0x38, %rsp
0x100001d0e <+94>: popq %r13
0x100001d10 <+96>: popq %rbp
0x100001d11 <+97>: retq
此时可以发现,即使我们改变的变量本质是存储属性,但编译器仍然为其生成了 setter
方法,并在该行执行 si
进入后可以发现,即 setter
方法中先调用了 willSet
,并在随后调用了 didSet
。
另外需要注意的是,初始化方法或在属性定义时给予的默认值时,将不会触发属性观察器;另外,在 willSet
中如果继续编写更改原有变量值的代码时,既不会再次触发属性观察器,但更改也不会生效,但在 didSet
中,虽然也不会再次触发属性观察器,但更改将会生效:
struct Foo {
var bar: Int {
willSet {
// WARNING: Attempting to store to property 'bar' within its own willSet, which is about to be overwritten by the new value
bar = 1000
print("\(bar) will set to \(newValue)")
}
didSet {
bar = 2000
print("\(oldValue) did set to \(bar)")
bar = 3000
}
}
}
var foo = Foo(bar: 10)
foo.bar = 20
print(foo.bar)
// 10 will set to 20
// 10 did set to 2000
// 3000
如上,虽然我们可以了解这一行为,但在属性观察器中再次进行赋值的操作本身就有些诡异,因此应当避免。
计算属性、属性观察器与 inout
在上面的示例中,我们通常直接通过实例对象对变量本身进行了更改,并触发了属性观察器,那么如果我们将它们传递给含有 inout
参数的方法并进行更改时,还会调用相应的 willSet
和 didSet
方法吗?
首先我们创建一个名为 Foo
的结构体类型,其中包含普通的存储属性、带有属性观察器的属性、和计算属性,并创建名为 inoutFunc(_:)
带有 inout
修饰参数的方法:
struct Foo: CustomStringConvertible {
var a = 1
var b = 1 {
willSet {
print("b(\(b)) will set to \(newValue)")
}
didSet {
print("b(\(oldValue)) did set to \(b)")
}
}
var c: Int {
get {
print("c get")
return a * 10
}
set {
print("c set")
a = newValue / 10
}
}
var description: String {
"a: \(a), b: \(b), c: \(c)"
}
}
func inoutFunc(_ param: inout Int) {
param = 100
}
var foo = Foo()
print(foo)
// c get
// a: 1, b: 1, c: 10
首先我们尝试将不带属性观察器的变量传递到 inoutFunc
中:
inoutFunc(&foo.a) // BREAKPOINT 🔴
print(foo)
// c get
// a: 100, b: 1, c: 1000
尝试运行后在断点处查看汇编:
-> 0x100001958 <+296>: movq %rax, %rdi
0x10000195b <+299>: leaq -0x30(%rbp), %rsi
0x10000195f <+303>: movl $0x21, %edx
0x100001964 <+308>: callq 0x100003a3c ; symbol stub for: swift_beginAccess
; 将 rip 寄存器中的地址值加上 0x6850(根据注释,该地址对应的内容为 demo.foo)并存储到 rdi 寄存器(函数第一个参数)中
0x100001969 <+313>: leaq 0x6850(%rip), %rdi ; demo.foo : demo.Foo
0x100001970 <+320>: callq 0x1000026f0 ; demo.inoutFunc(inout Swift.Int) -> () at main.swift:33
由于结构体变量的首地址就等同于其中第一个声明的变量的地址,因此此时存储属性即按地址传递到 inout
修饰参数的方法中。那么带有属性观察器的存储属性呢?
// inoutFunc(&foo.a)
inoutFunc(&foo.b) // BREAKPOINT 🔴
print(foo)
// b(1) will set to 100
// b(1) did set to 100
// c get
// a: 1, b: 100, c: 10
通过输出结果,我们可以看出属性观察器同样也被执行了。尝试运行后在断点处查看汇编:
-> 0x100001942 <+50>: movq %rcx, %rdi
0x100001945 <+53>: leaq -0x20(%rbp), %rax
0x100001949 <+57>: movq %rsi, -0x58(%rbp)
0x10000194d <+61>: movq %rax, %rsi
0x100001950 <+64>: movl $0x21, %edx
0x100001955 <+69>: movq -0x58(%rbp), %rcx
0x100001959 <+73>: callq 0x100003a2c ; symbol stub for: swift_beginAccess
0x10000195e <+78>: movq 0x6863(%rip), %rax ; demo.foo : demo.Foo + 8
0x100001965 <+85>: movq %rax, -0x28(%rbp)
0x100001969 <+89>: leaq -0x28(%rbp), %rdi
0x10000196d <+93>: callq 0x1000026e0 ; demo.inoutFunc(inout Swift.Int) -> () at main.swift:33
0x100001972 <+98>: movq -0x28(%rbp), %rdi
0x100001976 <+102>: leaq 0x6843(%rip), %r13 ; demo.foo : demo.Foo
; 将调用 setter
0x10000197d <+109>: callq 0x100001b30 ; demo.Foo.b.setter : Swift.Int at main.swift:6
与之前不同的是,当执行完 inoutFunc
后,setter
仍然得到了调用,但具体原因我们先继续看下计算属性。我们知道计算属性并不需要内存来存储结果,而其结果来自于其它属性的计算。因此即使 inoutFunc
也并不会获取到变量 c
的内存地址:
// inoutFunc(&foo.a)
// inoutFunc(&foo.b)
inoutFunc(&foo.c) // BREAKPOINT 🔴
print(foo)
// c get
// c set
// c get
// a: 10, b: 1, c: 100
尝试运行后在断点处查看汇编:
-> 0x100001932 <+50>: movq %rcx, %rdi
0x100001935 <+53>: leaq -0x20(%rbp), %rax
0x100001939 <+57>: movq %rsi, -0x58(%rbp)
0x10000193d <+61>: movq %rax, %rsi
0x100001940 <+64>: movl $0x21, %edx
0x100001945 <+69>: movq -0x58(%rbp), %rcx
0x100001949 <+73>: callq 0x100003a2c ; symbol stub for: swift_beginAccess
0x10000194e <+78>: movq 0x686b(%rip), %rdi ; demo.foo : demo.Foo
0x100001955 <+85>: movq 0x686c(%rip), %rsi ; demo.foo : demo.Foo + 8
; 将调用计算属性的 getter
0x10000195c <+92>: callq 0x1000020d0 ; demo.Foo.c.getter : Swift.Int at main.swift:17
0x100001961 <+97>: movq %rax, -0x28(%rbp)
0x100001965 <+101>: leaq -0x28(%rbp), %rdi
; 将调用 inoutFunc
0x100001969 <+105>: callq 0x1000026e0 ; demo.inoutFunc(inout Swift.Int) -> () at main.swift:33
0x10000196e <+110>: movq -0x28(%rbp), %rdi
0x100001972 <+114>: leaq 0x6847(%rip), %r13 ; demo.foo : demo.Foo
; 将调用计算属性的 setter
0x100001979 <+121>: callq 0x100002220 ; demo.Foo.c.setter : Swift.Int at main.swift:22
根据汇编,当遇到计算属性或带有存储属性观察器的存储属性时,将先通过 getter
将值拷贝到一个外界的局部变量上,再将该变量的地址传递到 inoutFunc
方法中,得到结果后外界变量将重新通过 setter
将值设置回原有的计算属性。因此属性观察器也是同理被触发的,而由于属性观察器只包含对设置值的不同时机的监听,因此看不到 getter
的过程。
类型属性
类型属性指的是通过类型直接访问的属性。我们在结构体、类、枚举中均可以定义类型属性,通常使用 static
关键字;在类中,如果定义的类型属性是计算属性,那么也可以使用 class
关键字:
struct Foo {
static var foo: Int = 0
}
class Bar {
static var bar1: Int = 0
static let bar2: Int = 0
class var bar3: Int {
0
}
}
enum Baz {
static var baz = 0
}
print(Foo.foo) // 0
print(Bar.bar1) // 0
print(Bar.bar2) // 0
print(Bar.bar3) // 0
print(Baz.baz) // 0
类型属性其实类似于全局变量,即只会在内存中保存一份,且实际在内存中与全局变量存储在同一位置:
var foo = 10
class Bar {
static var bar1: Int = 20
}
Bar.bar1 = 21 // BREAKPOINT 🔴
Bar.bar1 = 22
var baz = 30
尝试运行后在断点处查看汇编:
demo`main:
0x100002940 <+0>: pushq %rbp
0x100002941 <+1>: movq %rsp, %rbp
0x100002944 <+4>: subq $0x50, %rsp
; 全局变量 foo:移动立即数 10 到 rip + 0x5955(0x1000082A8)
0x100002948 <+8>: movq $0xa, 0x5955(%rip) ; Swift51Overrides + 180
0x100002953 <+19>: movl %edi, -0x34(%rbp)
0x100002956 <+22>: movq %rsi, -0x40(%rbp)
0x10000295a <+26>: callq 0x100002a20 ; demo.Bar.bar.unsafeMutableAddressor : Swift.Int at main.swift
0x10000295f <+31>: xorl %ecx, %ecx
0x100002961 <+33>: movq %rax, %rdx
0x100002964 <+36>: movq %rdx, %rdi
0x100002967 <+39>: leaq -0x18(%rbp), %rsi
0x10000296b <+43>: movl $0x21, %edx
0x100002970 <+48>: movq %rax, -0x48(%rbp)
0x100002974 <+52>: callq 0x100003cd2 ; symbol stub for: swift_beginAccess
0x100002979 <+57>: movq -0x48(%rbp), %rax
; (lldb) register read rax => rax = 0x00000001000082b0 demo`static demo.Bar.bar : Swift.Int
; 类型属性 Bar.bar:移动立即数 21 到 rax 寄存器中地址对应的内存空间
-> 0x10000297d <+61>: movq $0x15, (%rax)
0x100002984 <+68>: leaq -0x18(%rbp), %rdi
0x100002988 <+72>: callq 0x100003cde ; symbol stub for: swift_endAccess
0x10000298d <+77>: callq 0x100002a20 ; demo.Bar.bar.unsafeMutableAddressor : Swift.Int at main.swift
0x100002992 <+82>: xorl %r8d, %r8d
0x100002995 <+85>: movl %r8d, %ecx
0x100002998 <+88>: movq %rax, %rdx
0x10000299b <+91>: movq %rdx, %rdi
0x10000299e <+94>: leaq -0x30(%rbp), %rsi
0x1000029a2 <+98>: movl $0x21, %edx
0x1000029a7 <+103>: movq %rax, -0x50(%rbp)
0x1000029ab <+107>: callq 0x100003cd2 ; symbol stub for: swift_beginAccess
0x1000029b0 <+112>: movq -0x50(%rbp), %rax
; (lldb) register read rax => rax = 0x00000001000082b0 demo`static demo.Bar.bar : Swift.Int
; 类型属性 Bar.bar:移动立即数 22 到 rax 寄存器中地址对应的内存空间
0x1000029b4 <+116>: movq $0x16, (%rax)
0x1000029bb <+123>: leaq -0x30(%rbp), %rdi
0x1000029bf <+127>: callq 0x100003cde ; symbol stub for: swift_endAccess
0x1000029c4 <+132>: xorl %eax, %eax
; 全局变量 baz:移动立即数 30 到 rip + 0x58e7(0x1000082B8)
0x1000029c6 <+134>: movq $0x1e, 0x58e7(%rip) ; static demo.Bar.bar : Swift.Int + 4
0x1000029d1 <+145>: addq $0x50, %rsp
0x1000029d5 <+149>: popq %rbp
0x1000029d6 <+150>: retq
; 全局变量 foo:0x1000082A8
; 类型属性 Bar.bar:0x00000001000082b0(foo:0x1000082B0)
; 全局变量 baz:0x1000082B8
如上,三个变量均是在内存中占用了连续的存储空间,也证明了类型属性本质也是存储在全局区,其在整个程序生命周期中只会有一份。只是类型属性定义类的内部,需要通过类名来访问,以及可以额外使用一些访问控制。
需要注意的是,由于类型属性非针对于某一实例对象,因此其必须在声明时即给定初始值;类型属性如果同时为存储属性,默认即为 lazy
延迟:
struct Foo {
static var foo: Int = 0
static var calculateFoo: Int = {
print("calculateFoo init")
return 0
}()
}
// 0
// calculateFoo init
但此时的延迟存储属性是线程安全的:
import Foundation
struct Foo {
static var foo: Int = 0
static var calculateFoo: Int = {
print("calculateFoo init")
return 0
}()
}
print(Foo.foo)
DispatchQueue.concurrentPerform(iterations: 5) { _ in
print("--- \(Thread.current) ---")
print(Foo.calculateFoo)
}
// 0
// --- <NSThread: 0x1020be860>{number = 4, name = (null)} ---
// calculateFoo init
// 0
// --- <NSThread: 0x1007042d0>{number = 3, name = (null)} ---
// 0
// --- <NSThread: 0x1021040c0>{number = 2, name = (null)} ---
// 0
// --- <NSThread: 0x10200ab30>{number = 1, name = main} ---
// 0
// --- <NSThread: 0x1022040c0>{number = 5, name = (null)} ---
// 0
// 0
如上,即使同时 5 个线程并发访问,calculateFoo
也将被保证只初始化一次。那么如何证明其是线程安全的呢?
class Bar {
static var bar: Int = 20 // BREAKPOINT 🔴
}
Bar.bar = 21 // BREAKPOINT 🔴
尝试运行后在断点处查看汇编:
demo`main:
0x100002990 <+0>: pushq %rbp
0x100002991 <+1>: movq %rsp, %rbp
0x100002994 <+4>: subq $0x30, %rsp
0x100002998 <+8>: movl %edi, -0x1c(%rbp)
0x10000299b <+11>: movq %rsi, -0x28(%rbp)
-> 0x10000299f <+15>: callq 0x100002a20 ; demo.Bar.bar.unsafeMutableAddressor : Swift.Int at main.swift
; 调用后返回值(即 Bar.bar 的内存地址)存储在 rax 寄存器
0x1000029a4 <+20>: xorl %ecx, %ecx
0x1000029a6 <+22>: movq %rax, %rdx
0x1000029a9 <+25>: movq %rdx, %rdi
0x1000029ac <+28>: leaq -0x18(%rbp), %rsi
0x1000029b0 <+32>: movl $0x21, %edx
0x1000029b5 <+37>: movq %rax, -0x30(%rbp)
0x1000029b9 <+41>: callq 0x100003cd2 ; symbol stub for: swift_beginAccess
0x1000029be <+46>: movq -0x30(%rbp), %rax
; (lldb) register read rax => rax = 0x0000000100008378 demo`static demo.Bar.bar : Swift.Int
; 移动立即数 21 到 rax(0x0000000100008378,即 Bar.bar 的内存地址)
0x1000029c2 <+50>: movq $0x15, (%rax)
0x1000029c9 <+57>: leaq -0x18(%rbp), %rdi
0x1000029cd <+61>: callq 0x100003cde ; symbol stub for: swift_endAccess
0x1000029d2 <+66>: xorl %eax, %eax
0x1000029d4 <+68>: addq $0x30, %rsp
0x1000029d8 <+72>: popq %rbp
0x1000029d9 <+73>: retq
demo`Bar.bar.unsafeMutableAddressor:
0x100002a20 <+0>: pushq %rbp
0x100002a21 <+1>: movq %rsp, %rbp
-> 0x100002a24 <+4>: cmpq $-0x1, 0x587c(%rip) ; Swift51Overrides + 183
0x100002a2c <+12>: sete %al
0x100002a2f <+15>: testb $0x1, %al
0x100002a31 <+17>: jne 0x100002a35 ; <+21> at main.swift:4:16
0x100002a33 <+19>: jmp 0x100002a3e ; <+30> at main.swift
; 将 rip + 0x593c(0x100008378,即 Bar.bar 的内存地址)放在 rax 寄存器中(即返回值)
0x100002a35 <+21>: leaq 0x593c(%rip), %rax ; static demo.Bar.bar : Swift.Int
0x100002a3c <+28>: popq %rbp
; 函数返回
0x100002a3d <+29>: retq
; 将类型属性初始化代码所在闭包的函数地址存储在 rax 寄存器
0x100002a3e <+30>: leaq -0x45(%rip), %rax ; globalinit_33_8E34543FA00B3A97BCFBD7BFA4EE5AD5_func0 at main.swift
0x100002a45 <+37>: leaq 0x585c(%rip), %rdi ; globalinit_33_8E34543FA00B3A97BCFBD7BFA4EE5AD5_token0
; 移动 rax 寄存器中的内容到 rsi 寄存器(作为 swift_once 函数第一个参数)
0x100002a4c <+44>: movq %rax, %rsi
; 从 swift_once 开始 si,直到进入 libswiftCore.dylib`swift_once:
0x100002a4f <+47>: callq 0x100003ce4 ; symbol stub for: swift_once
; 跳转到 0x100002a35 ⬆️
0x100002a54 <+52>: jmp 0x100002a35 ; <+21> at main.swift:4:16
libswiftCore.dylib`swift_once:
-> 0x7fff6806a820 <+0>: pushq %rbp
0x7fff6806a821 <+1>: movq %rsp, %rbp
0x7fff6806a824 <+4>: cmpq $-0x1, (%rdi)
0x7fff6806a828 <+8>: jne 0x7fff6806a82c ; <+12>
0x7fff6806a82a <+10>: popq %rbp
0x7fff6806a82b <+11>: retq
0x7fff6806a82c <+12>: movq %rsi, %rax
0x7fff6806a82f <+15>: movq %rdx, %rsi
0x7fff6806a832 <+18>: movq %rax, %rdx
; swift_once 内部实际调用了 dispatch_once_f
0x7fff6806a835 <+21>: callq 0x7fff680bf19c ; symbol stub for: dispatch_once_f
0x7fff6806a83a <+26>: popq %rbp
0x7fff6806a83b <+27>: retq
0x7fff6806a83c <+28>: nop
0x7fff6806a83d <+29>: nop
0x7fff6806a83e <+30>: nop
0x7fff6806a83f <+31>: nop
demo`globalinit_33_8E34543FA00B3A97BCFBD7BFA4EE5AD5_func0:
0x100002a00 <+0>: pushq %rbp
0x100002a01 <+1>: movq %rsp, %rbp
; 移动立即数 20 到 rip + 0x5969(0x100008378,即 Bar.bar 的内存地址)
-> 0x100002a04 <+4>: movq $0x14, 0x5969(%rip) ; getExistentialTypeMetadata(swift::ProtocolClassConstraint, swift::TargetMetadata<swift::InProcess> const*, unsigned long, swift::TargetProtocolDescriptorRef<swift::InProcess> const*)::$_3::operator()() const::TheLazy + 12
0x100002a0f <+15>: popq %rbp
0x100002a10 <+16>: retq
因此类型属性默认即是 lazy
延迟初始化的,并由 swift_once
内的 dispatch_once_f
保证了线程安全。
类型属性通常可以用于单例模式,即整个生命周期内仅初始化一次:
class SomeManager {
static var manager: SomeManager = {
var manager = SomeManager()
manager.foo = 10
return manager
}()
var foo: Int = 0
private init() {}
func bar() {
print(#function, foo)
}
}
SomeManager.manager.bar()
// bar() 10