一、存储属性
1.1 存储属性概述
存储属性 是一个作为特定类和结构体实例一部分的常量或变量。存储属性要么是变量存储属性 (由 var 关键字引入)要么是常量存储属性(由 let 关键字引入)。存储属性这里没有什么特 别要强调的,因为随处可⻅
class SSLPerson {
var age: Int
var name: String
}
复制代码
比如这里的 age
和 name
就是我们所说的存储属性,这里我们需要加以区分的是 let
和 var
两者的区别:从定义上:
let
用来声明常量,常量的值一旦设置好便不能再被更改var
用来声明变量,变量的值可以在将来设置为不同的值。
1.2 let 和 var 案例
class 案例:
struct 案例:
1.3 let 和 var 比较
1.3.1 汇编角度分析
先创建代码:
var age = 18
let x = 20
复制代码
进行汇编调试:
从汇编调试来看没有区别,都是将值存储到了寄存器中,下面通过 lldb 调试进行分析
lldb 调试来看也没有什么区别,都是存储在了 __DATA.__common
中,而且是相邻的地址。
1.3.2 sil 角度分析
将 main.swift 编译成 main.sil :
@_hasStorage @_hasInitialValue var age: Int { get set }
@_hasStorage @_hasInitialValue let x: Int { get }
...
复制代码
- 通过 sil 我们可以发现,var 修饰的属性有 get 和 set 方法
- let 修饰的属性只有 get 方法,所有 let 修饰的属性不能修改。
二、计算属性
2.1 计算属性 概述
存储的属性是最常⻅的,除了存储属性,类、结构体和枚举也能够定义计算属性,计算属性并不存储值,他们提供 getter
和 setter
来修改和获取值。对于存储属性来说可以是常量或变量,但计算属性必须定义为变量。于此同时我们书写计算属性时必须包含类型,因为编译器需要知道期望返回值是什么。
下面的 area 就是计算属性:
struct Square {
// 实例中占据内存
var width: Double
let height: Double
// 实例中不占用内存
var area: Double {
get {
return width * height
}
set {
self.width = newValue // newValue : 编译器默认生成
}
}
}
var s = Square(width: 10, height: 20)
s.area = 30
复制代码
2.2 计算属性 sil分析
将 main.swift 编译成 main.sil :
struct Square {
@_hasStorage var width: Double { get set }
@_hasStorage let height: Double { get }
var area: Double { get set }
}
复制代码
可以看到计算属性没有 @_hasStorage
的标记
2.3 private(set) 分析
将 area 改为 private(set)
修饰:
struct Square {
// 实例中占据内存
var width: Double
let height: Double
private(set) var area: Double = 40
}
复制代码
生成 sil 文件:
struct Square {
@_hasStorage var width: Double { get set }
@_hasStorage let height: Double { get }
@_hasStorage @_hasInitialValue private(set) var area: Double { get set }
}
复制代码
通过 sil 可以发现,private(set)
修饰后,依然是存储属性,只不过 set
方法是私有的。
三、属性观察者
3.1 属性观察者 分析
属性观察者会观察用来观察属性值的变化,一个 willSet
当属性将被改变调用,即使这个值与 原有的值相同,而 didSet
在属性已经改变之后调用。它们的语法类似于 getter 和 setter。
看下面的代码 willSet 和 didSet 将会被调用:
class SubjectName {
var subjectName: String = ""{
willSet{
print("subjectName will set value \(newValue)")
}
didSet{
print("subjectName has been changed \(oldValue)")
}
}
}
let s = SubjectName()
s.subjectName = "Swift"
输出:
subjectName will set value Swift
subjectName has been changed
复制代码
观察者内部是怎么实现的呢,将上面代码编译成 sil :
class SubjectName {
@_hasStorage @_hasInitialValue var subjectName: String { get set }
}
// SubjectName.subjectName.setter
sil hidden @$s4main11SubjectNameC07subjectC0SSvs : $@convention(method) (@owned String, @guaranteed SubjectName) -> () {
...
// function_ref SubjectName.subjectName.willset
...
// function_ref SubjectName.subjectName.didset
...
}
复制代码
可以看到原来 willset
和 didset
都是在 setter
方法中被调用了。
3.2 初始化期间设置属性
- 这里我们在使用属性观察器的时候,需要注意的一点是在初始化期间设置属性时不会调用
willSet
和didSet
观察者; - 只有在为完全初始化的实例分配新值时才会调用它们。
- 运行下面这段代码,你会发现当前并不会有任何的输出。
class SubjectName {
var subjectName: String = ""{
willSet{
print("subjectName will set value \(newValue)")
} didSet{
print("subjectName has been changed \(oldValue)")
}
}
init(subjectName: String) {
self.subjectName = subjectName;
}
}
let s = SubjectName(subjectName: "Swift进阶")
复制代码
为什么会出现这种情况呢,查看 sil 文件:
// SubjectName.init(subjectName:)
sil hidden @$s4main11SubjectNameC07subjectC0ACSS_tcfc : $@convention(method) (@owned String, @owned SubjectName) -> @owned SubjectName {
...
%13 = ref_element_addr %1 : $SubjectName, #SubjectName.subjectName // user: %14
%14 = begin_access [modify] [dynamic] %13 : $*String // users: %16, %15, %18
%15 = load %14 : $*String // user: %17
...
}
复制代码
通过 sil 文件,我们发现初始化时并没有调用 setter 方法,而是对属性的地址直接进行了赋值,所以监听方法不会被调用。
3.3 计算属性观察者
上面的属性观察者只是对存储属性起作用,如果我们想对计算属性起作用怎么办?很简单,只需将相关代码添加到属性的 setter。来看这段代码
class Square {
var width: Double
var area: Double {
get {
return width * width
}
set {
self.width = sqrt(newValue)
}
}
init(width: Double) {
self.width = width
}
}
复制代码
3.4 继承属性观察者
继承属性下的观察者是什么样的呢,看下面代码:
class SSLTeacher {
var age: Int {
willSet{
print("age will set value \(newValue)")
} didSet{
print("age has been changed \(oldValue)")
}
}
var height: Double
init(_ age: Int, _ height: Double) {
self.age = age
self.height = height
}
}
class SSLParTimeTeacher: SSLTeacher {
override var age: Int {
willSet{
print("override age will set value \(newValue)")
} didSet{
print("override age has been changed \(oldValue)")
}
}
var subjectName: String
init(_ subjectName: String) {
self.subjectName = subjectName
super.init(10, 180)
self.age = 20
}
}
var t = SSLParTimeTeacher("Swift")
复制代码
运行程序,可以得到继承属性下方法的执行顺序:
override age will set value 20 // 子类的 will
age will set value 20 // 父类的 will
age has been changed 10 // 父类的 did
override age has been changed 10 // 子类的 did
复制代码
四、延迟存储属性
4.1 延迟存储属性的使用
-
用关键字
lazy
来标识一个延迟存储属性,延迟存储属性必须有初始值:class Person { lazy var age: Int = 18 } 复制代码
-
延迟存储属性的初始值在其第一次使用时才进行计算
-
如下所示,初始化以后 age 是没有值的:
-
调用 age 的 getter 方法后,可以看到 age 已经有值了:
-
4.2 sil 原理探索
4.2.1 初始化过程
首先生成 sil 文件,观察 age 的声明是有个 ?
符号的,说明 age 是个可选类型
再看 age 的初始化,默认是被赋值了 Optional.none
也就是 0
4.2.2 调用过程
查看 age 的 getter 方法,看下它的调用过程:
// Person.age.getter
sil hidden [lazy_getter] [noinline] @$s4main6PersonC3ageSivg : $@convention(method) (@guaranteed Person) -> Int {
// %0 "self" // users: %14, %2, %1
bb0(%0 : $Person):
debug_value %0 : $Person, let, name "self", argno 1 // id: %1
%2 = ref_element_addr %0 : $Person, #Person.$__lazy_storage_$_age // user: %3
%3 = begin_access [read] [dynamic] %2 : $*Optional<Int> // users: %4, %5
%4 = load %3 : $*Optional<Int> // user: %6
end_access %3 : $*Optional<Int> // id: %5
switch_enum %4 : $Optional<Int>, case #Optional.some!enumelt: bb1, case #Optional.none!enumelt: bb2 // id: %6
// %7 // users: %9, %8
bb1(%7 : $Int): // Preds: bb0
debug_value %7 : $Int, let, name "tmp1" // id: %8
br bb3(%7 : $Int) // id: %9
bb2: // Preds: bb0
%10 = integer_literal $Builtin.Int64, 18 // user: %11
%11 = struct $Int (%10 : $Builtin.Int64) // users: %18, %13, %12
debug_value %11 : $Int, let, name "tmp2" // id: %12
%13 = enum $Optional<Int>, #Optional.some!enumelt, %11 : $Int // user: %16
%14 = ref_element_addr %0 : $Person, #Person.$__lazy_storage_$_age // user: %15
%15 = begin_access [modify] [dynamic] %14 : $*Optional<Int> // users: %16, %17
store %13 to %15 : $*Optional<Int> // id: %16
end_access %15 : $*Optional<Int> // id: %17
br bb3(%11 : $Int) // id: %18
复制代码
- getter 方法调用过程先会获取到 age 的地址,看它有没有值
- 如果没有值也就是
Optional.none
,就会调用bb2
,这里会得到值并赋值到 age 的地址空间 - 如果有值,就会调用
bb1
,将值直接返回 - 所以延迟存储属性并不是线程安全的。
五、类型属性
5.1 类型属性初探
- 类型属性其实就是一个全局变量
- 类型属性只会被初始化一次
class SSLTeacher {
// 只被初始化一次
static var age: Int = 18
}
// 可以修改
SSLTeacher.age = 30
复制代码
5.2 sil & 源码分析
生成 sil 文件,初始化调用时可以看到 builtin "once"
的调用
builtin "once"
的调用在源码中就是 swift_once
的调用,打开 swift 源码,找到 swift_once
的实现:
void swift::swift_once(swift_once_t *predicate, void (*fn)(void *),
void *context) {
#ifdef SWIFT_STDLIB_SINGLE_THREADED_RUNTIME
if (! *predicate) {
*predicate = true;
fn(context);
}
#elif defined(__APPLE__)
dispatch_once_f(predicate, context, fn);
#elif defined(__CYGWIN__)
_swift_once_f(predicate, context, fn);
#else
std::call_once(*predicate, [fn, context]() { fn(context); });
#endif
}
复制代码
源码的实现可以看到dispatch_once_f
,这不是就是我们的 GCD 吗!!
5.3 单例的实现
class SSLTeacher {
static let sharedInstance = SSLTeacher()
// 指定初始化器私有化,外界访问不到
private init(){}
}
SSLTeacher.sharedInstance
复制代码
六、属性与 Mach-O
6.1 fieldDescriptor && FieldRecord
在 上一篇文章 探索方法调度的过程中我们认识了 typeDescriptor
,这里面记录了 V-Table
的相关信息,接下来我们需要认识一下 typeDescriptor
中的 fieldDescriptor
struct TargetClassDescriptor {
var flags: UInt32
var parent: UInt32
var name: Int32
var accessFunctionPointer: Int32
var fieldDescriptor: Int32
var superClassType: Int32
var metadataNegativeSizeInWords: UInt32
var metadataPositiveSizeInWords: UInt32
var numImmediateMembers: UInt32
var numFields: UInt32
var fieldOffsetVectorOffset: UInt32
var Offset: UInt32
var size: UInt32
//V-Table
}
复制代码
fieldDescriptor
记录了当前的属性信息,其中 fieldDescriptor
在源码中的结构如下:
struct FieldDescriptor {
MangledTypeName int32
Superclass int32
Kind uint16
FieldRecordSize uint16
NumFields uint32
FieldRecords [FieldRecord]
}
复制代码
其中 NumFields
代表当前有多少个属性,FieldRecords
记录了每个属性的信息,FieldRecords
的结构体如下:
struct FieldRecord {
Flags uint32
MangledTypeName int32
FieldName int32
}
复制代码
6.2 属性在 Mach-O 文件的位置信息
先创建一个类,编译出 Mach-O 文件
class SSLTeacher {
var age = 18
var age2 = 20
}
复制代码
接下来在 Mach-O 文件中,计算并寻找属性的相关信息
-
先计算出
typeDescriptor
在 Mach-O 中的地址FFFFFF2C + 3F40 = 0x100003E6C 0x100003E6C - 0x100000000 = 3E6C 复制代码
-
定位到
3E6C
-
平移 4 个 4 字节,找到
fieldDescriptor
,并计算fieldDescriptor
的地址3E7C + 9C = 3F18 复制代码
-
定位
fieldDescriptor
, 并偏移 4 个 4 字节找到FieldRecords
-
计算 age 的
FieldName
在 Mach-O 中的地址3F30 + FFFFFFDF = 0x100003F0F 0x100003F0C - 0x1000000000 = 3F0F 复制代码
-
成功定位 age 的
FieldName
在 Mach-O 中的位置!!