一、存储属性
存储属性是一个作为特定类和结构体实例一部分的常量或变量。存储属性要么是变量存储属性(由 var 关键字引入)要么是常量存储属性(由 let 关键字引入)。存储属性这里没有什么特别要强调的,因为随处可见。
struct Person {
let name: String
var age: Int
}
比如这里的 name 和 age 就是我们所说的存储属性,这里我们需要加以区分的是let 和var 两者的区别:
从定义上: let 用来声明常量,常量的值一但设置好便不能再被更改;
var 用来声明变量,变量的值可以在将来再修改。
1. 从汇编的角度分析var和let
断点到汇编可以看到没啥区别,var和let都是存地址,我们再看一下地址:
都是在全局区的地址,只是差了8字节,存储的值不同,其它都一样。
2. 从SIL的角度分析var和let
由此可以发现var和let的本质区别在于编译器有没有生成set方法,如果没有set方法,你就无法给该属性赋值,这和let的定义也是符合的。
二、计算属性
存储属性是最常见的,除了存储属性,类、结构体和枚举也能够定义计算属性,计算属性并不存储值,他们提供getter 和 setter 来修改和获取值。对于存储属性来说可以是常量或变量,但计算属性必须定义为变量,而且声明计算属性时候必须包含类型,因为编译器在类型推导时需要知道返回值是什么。
struct Square {
/// 存储属性 内存中占8字节
var width: Double
/// 计算属性
var area: Double {
get {
width * width
}
set {
width = sqrt(newValue)
}
}
}
var s = Square(width: 5)
print(s.area)
s.area = 36
print(s.width)
1. 汇编分析
很明显,area的setter和getter方法都是静态调用。
2. SIL分析
由此可以看到width是存储属性,在Square中占8字节,编译器已经生成了set和get。
这里我们需要注意的是area与width的区别:
area的setter中newValue是默认参数名,携带的是最新的值,当然也可以自定义参数名。
area本质上是在生成方法,setter和getter是直接调用方法并将当前实例Square的地址传入。
三、属性观察者
属性观察者会观察用来观察属性值的变化,一个willSet 当属性将被改变调用,即使这个值与原有的值相同,而 didSet 在属性已经改变之后调用。它们的语法类似于 getter 和 setter。
class Person {
var age: Int {
willSet {
print("Person willSet", newValue)
}
didSet {
print("Person didSet", oldValue)
}
}
init(age: Int) {
self.age = age
}
}
class Teacher: Person {
override var age: Int {
willSet {
print("Teacher willSet", newValue)
}
didSet {
print("Teacher didSet", oldValue)
}
}
}
let t = Teacher(age: 3)
t.age = 5
执行结果:
Teacher willSet 5
Person willSet 5
Person didSet 3
Teacher didSet 3
这里需要注意的是有继承关系的类的属性观察者的调用顺序:
子类willSet > 父类willSet > 父类didSet > 子类didSet
其次初始化实例时属性观察者是不会回调的,我们可以从SIL分析一下。
1. 分析SIL
由此可以看出子类属性在setter时先调自己的willset然后调父类的setter,而父类setter中先调父类自己的willset再调自己的didset,最后子类再调自己didset。这与代码的执行结果是匹配的。
Teacher在初始化时最后调到Person.init中,由此可以看到age是直接通过内存地址赋值,并不是调setter方法。因为此时Person还没有初始化完成,如果此时访问某些属性的setter或getter可能会造成内存访问错误,所以这么设计是合理的。
四、延迟存储属性(懒加载)
-
延迟存储属性的初始值在其第一次使用时才进行计算。
-
用关键字
lazy来标识一个延迟存储属性。
此时查看Person实例:
注意前16字节为metadata,这里我们忽略,主要看后面age的值,很明第一次断点时age还没有值,我们接着看下一个断点:
当我们访问age时,age中才有值。
1. SIL分析
这里我们需要注意几个点:
-
final标记 -
__lazy_storage_标记 -
Int?可选类型
进一步查看可以发现,age的默认值就是可选类型的空(可选类型本质是enum)。那么当我们访问age时发生了什么?
在bb0中我们可以看到访问age时其实是从age地址中将值取出来放到寄存器%4中,让后通过switch_enum来匹配,有值走bb1,没值走bb2。第一次访问时因为没值所以走bb2来进行赋值操作,bb2中是创建一个Int类型的值3,然后将该值给到了枚举变量,最后将枚举变量赋值给age。第二次访问时因为有值所以走bb1,bb1调了bb3其实就是将age已有的值返回(并没有走bb2重新赋值)。
此时我们可以思考一个问题,延迟存储属性时线程安全的吗?答案是否定的。如果该属性是直接返一个常量(参考age),那么影响并不大,但是如果换一种方式,比如通过闭包表达式(函数调用)创建值,就像这样:
import UIKit
class Person {
lazy var age: Int = {
print("init age")
return 3
}()
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let t = Person()
DispatchQueue.global().async {
print(t.age)
}
DispatchQueue.global().async {
print(t.age)
}
}
}
执行结果:
init age
3
init age
3
很明显,age被创建了2次。所以对于非线程安全的延迟存储属性,我们需要合理使用。
五、类型属性
-
类型属性只会被初始化一次。
-
用关键字
static来标识一个类型属性。class Person { static var age = 3 } print(Person.age) Person.age = 5 print(Person.age)
1. SIL分析
由此可以发现age被声明为全局变量,在Person中可以看到age属性只是为了让我们通过Person来访问它。那么给age赋值发生了什么,我们继续看:
在这里我们发现调用一个方法s4main6PersonC3ageSivau,我们搜索该方法:
在这里我们可以看到,首先获取了一个全局声明的token的全局地址放到%0,然后将%0做了指针转化放到%1,接着获取了一个globalinit方法的地址放到%2,再进行builtin once并携带两个参数%1和%2,最后将全局变量(age)的内存地址返回回去。
那么globalinit方法做了什么,我们搜索globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_func0继续往下看:
在这里我们看到,首先创建了一个全局变量s4main6PersonC3ageSivpZ,那么这个全局变量是什么?通过xcrun swift-demangle s4main6PersonC3ageSivpZ我们可以发现这个全局变量就是Person.age;然后将全局变量地址给%1,接下就是创建一个Int类型的值3并将该值存到%1,简单来说这个方法就是给age赋值。
回到该方法调用的地方,我们继续看builtin once,从字面来看这个builtin once就是执行一次的意思,它其实是编译器字段,我们可以通过swiftc main.swift -emit-ir将当前代码降级到IR来看:
我们可以找到这个方法s4main6PersonC3ageSivau,通过xcrun swift-demangle s4main6PersonC3ageSivau我们可以发现这个方法就是SIL中的Person.age.unsafeMutableAddressore,也就是说这个方法会调builtin once,也就是上图中的swift_once
,此时可以推测出这个swift_once方法2个入参分别是全局的token和globalinit方法。
我们继续往下,在Swift源码中搜索swift_once,最终在Once.cpp中可以看到:
这是啥?这不就是我们熟悉gcd,predicate是我们传入的方法,context是上下文,fn是我们传入的globalinit方法,
由此我们可以看出,swift_once本质上就是dispatch_once。
2. 总结
类型属性其实就是一个全局变量,它在初始化过程中只会被初始化一次。这也是Swift中单例可以这样写的原因:
class Manager {
static let shared = Manager()
private init() {}
}
因为底层在帮我们用dispatch_once来初始化,从而保证了它的唯一性。