我们研究过成员属性的一些具体实现细节,本文我们来研究下类型属性的底层逻辑。
基本语法
- 类型属性的语法和成员属性类似的地方包括:可以定义存储属性和计算属性,也可以添加存储属性监听器
struct Sequence {
static var first: Int = 1 // 存储属性
static var second: Int { // 计算属性
get {
return first
}
set {
first = newValue
}
}
static var third: Int = 3 { // 存储添加属性监听器
didSet {
print("third didSet")
}
willSet {
print("third willSet")
}
}
}
区别是类型属性要用
static
进行修饰
- 类型属性不能用
lazy
修饰,因为类型属性默认就是already-lazy global
swift_once
实现分析
类型属性默认是懒加载,我们来看看底层的实现逻辑。
<!-- 测试代码 -->
struct Sequence {
static var first: Int = 1
}
func test() {
Sequence.first = 2
}
test()
- 获取
Sequence.first
的内存地址:Sequence.first.unsafeMutableAddressor
- 如果地址不存在,利用
swift_once
进行变量的初始化
-
swift_once
底层调用的是dispatch_once_f
我们得知:编译器会封装一个初始化函数,作为
dispatch_once_f
的fn
参数进行初始化调用
-
fn
函数封装
// one-time initialization function for first
sil private [global_init_once_fn] @$s4main8SequenceV5first_WZ : $@convention(c) () -> () {
bb0:
alloc_global @$s4main8SequenceV5firstSivpZ // id: %0
%1 = global_addr @$s4main8SequenceV5firstSivpZ : $*Int // user: %4
%2 = integer_literal $Builtin.Int64, 1 // user: %3
%3 = struct $Int (%2 : $Builtin.Int64) // user: %4
store %3 to %1 : $*Int // id: %4
%5 = tuple () // user: %6
return %5 : $() // id: %6
}
通过SIL分析,我们得知:编译器会封装一个初始化函数,大体的实现逻辑是:
- 得到变量的内存地址, 类似于:
var ptr = withUnsafePointer(to: &Sequence.first) { $0 }
- 将1赋值给这个内存地址上,类似于:
ptr.pointee = 2
总结
- 编译器会将
var first: Int = 1
封装成一个函数,函数体是先获取变量指针,然后给指针所指的内存地址赋值为初始值 - 类型属性底层是通过
dispatch_once_f
进行初始化,确保只会初始化一次,并且是线程安全的
全局变量
从图一的编译器错误提示我们可以得知,类型属性本质就是全局变量,只是有访问权限限定。
let zero: Int = 0
struct Sequence {
static var first: Int = 1
}
我们利用实例代码进行分析。
SIL分析
@_hasStorage @_hasInitialValue var zero: Int { get set }
struct Sequence {
@_hasStorage @_hasInitialValue static var first: Int { get set }
init()
}
// zero
sil_global hidden @$s4main4zeroSivp : $Int
// static Sequence.first
sil_global hidden @$s4main8SequenceV5firstSivpZ : $Int
我们看到
SIL
语法中,全局变量和类型属性的定义是完全相同的。
内存验证
func test() {
let ptr1 = withUnsafePointer(to: &zero) { UnsafeRawPointer($0) }
let ptr2 = withUnsafePointer(to: &Sequence.first) { UnsafeRawPointer($0) }
print("\(ptr1) \(ptr2)")
}
// 0x100008000 0x100008008
通过查看内存地址,我们得到的结果是
zero
和Sequence.first
的内存地址是连续挨在一起的。zero
肯定是全局变量,所以Sequence.first
本质上也是一个全局变量。
全局变量的更多用法
既然类型属性是全局变量,那全局变量应该也可以是计算属性等。其实确实也是可以这样写的:
var zero: Int = 0
var one: Int {
get {
zero
}
set {
zero = newValue
}
}
var two: Int = 2 {
willSet {
}
didSet {
}
}
全局变量的语法和类型属性的语法也是一致的。
变量内存安全(参考地址)
前面我们看到了类型属性本质是通过swift_once
得到了变量内存地址指针。Swift
编译器可以(也仅仅只有编译器可以)获取到全局变量的内存地址指针。
为什么需要获取变量的内存地址指针呢?这涉及到内存安全的部分
Swift
会保证同时访问同一块内存时不会冲突,通过约束代码里对于存储地址的写操作,去获取那一块内存的访问独占权。避免了读写冲突。
变量内存安全是通过swift_beginAccess
和swift_endAccess
等方法类控制的。
swift_beginAccess
逻辑总结:
- 先将内存指针封装成
Access
对象Access
对象的封装的内存指针如果在SwiftTLSContext::get().accessSet
数组中不存在,说明目前没有其他方法占用该内存地址,可以访问,并且将Access
对象保存起来;Access
对象的封装的内存指针如果在SwiftTLSContext::get().accessSet
数组中存在,说明该内存地址已经有访问存在了,如果所有的访问都是读访问,则不认为是冲突,可以继续访问,否则就会报访问冲突错误。
swift_endAccess
逻辑总结:
将当前的访问从SwiftTLSContext::get().accessSet
数组中移除,也就是将本次内存访问移除。
总结
- 类型属性本质上是全局变量,只是访问权限有所限制
- 类型属性和全局变量可以是存储属性,计算属性,也可以添加属性监听器,但是不能添加懒加载的
lazy
关键字 - 类型属性是懒加载的,通过
dispatch_once_f
进行, 确保只会初始化一次,并且是线程安全的 - 编译器对类型属性和全局变量添加了内存安全的控制,避免了访问的读写冲突,使代码更加安全