Swift 属性
在Swift
中属性主要分为存储属性、计算属性、延迟存储属性、类型属性这四种,并且Swift
还提供了属性观察者,以便开发者能在属性的改变前后进行观察。下面我就来一一探索。
1. 存储属性
存储属性顾名思义,就是需要存储的一种属性,既然存储就要占用内存空间,下面我们通过一个例子来对其进行解释。
示例代码:
class Teacher {
let age: Int = 18
var age2: Int = 100
}
let t = Teacher()
我们通过我的上一篇文章对Swift
的类和对象的探索,我们可以知道Swift
对象的存储前16个字节是metadata
和refCounts
1.1 通过lldb查看存储属性
下面我们就通过lldb
调试来查看一下示例代码中t
这个对象的内存结构。
通过lldb
调试我们可以看到,我们的两个age
的值都存储在了连续的对象存储空间中(注:这里是16进制)
1.2 通过SIL文件查看存储属性
我们使用如下命令将我们的swift
文件编译成sil
文件,并打开(注:如果打开失败就设置一下用VSCode
打开sil
类型的文件就可以了)
swiftc -emit-sil main.swift >> ./main.sil && open main.sil
1.2.1 类
编译后的SIL代码:
class Teacher {
@_hasStorage @_hasInitialValue final let age: Int { get }
@_hasStorage @_hasInitialValue var age2: Int { get set }
@objc deinit
init()
}
在sil
代码中我们可以看到属性的前面加上了@_hasStorage
关键字,这是存储属性独有的,对应的是计算属性,就没有这个关键字。
1.2.2 get&set
get方法及分析:
// Teacher.age.getter
sil hidden [transparent] @main.Teacher.age.getter : Swift.Int : $@convention(method) (@guaranteed Teacher) -> Int {
// %0 "self" // users: %2, %1
bb0(%0 : $Teacher):
debug_value %0 : $Teacher, let, name "self", argno 1 // id: %1
// 找出实例内部物理实例变量的地址
%2 = ref_element_addr %0 : $Teacher, #Teacher.age // user: %3
// 开始访问内存
%3 = begin_access [read] [dynamic] %2 : $*Int // users: %4, %5
// 内存中的值加载到%4虚拟寄存器
%4 = load %3 : $*Int // user: %6
// 结束内存访问
end_access %3 : $*Int // id: %5
// 返回获取到的值
return %4 : $Int // id: %6
} // end sil function 'main.Teacher.age.getter : Swift.Int'
set方法及分析:
// Teacher.age.setter
sil hidden [transparent] @main.Teacher.age.setter : Swift.Int : $@convention(method) (Int, @guaranteed Teacher) -> () {
// %0 "value" // users: %6, %2
// %1 "self" // users: %4, %3
bb0(%0 : $Int, %1 : $Teacher):
debug_value %0 : $Int, let, name "value", argno 1 // id: %2
debug_value %1 : $Teacher, let, name "self", argno 2 // id: %3
// 找出实例内部物理实例变量的地址
%4 = ref_element_addr %1 : $Teacher, #Teacher.age // user: %5
// 开始访问内存
%5 = begin_access [modify] [dynamic] %4 : $*Int // users: %6, %7
// 将第一个参数%0的值存储到%5
store %0 to %5 : $*Int // id: %6
// 停止内存访问
end_access %5 : $*Int // id: %7
%8 = tuple () // user: %9
return %8 : $() // id: %9
} // end sil function 'main.Teacher.age.setter : Swift.Int'
1.3 小结
打印内存占用大小:
对象的内存结构
这个32其实就是8+8+8+8得到的,在Swift
中对象的本质是HeapObject
内部有两个属性metadata
占用8字节,refCounts
占用8字节,Swift
中Int
实际是个结构体,占用8自己,这里两个Int
就是16字节。
综上所述:
- 存储属性是占用对象的分配的内存空间
2. 计算属性
计算属性,指的就是需要通过计算得到一些想要的值,那么对于属性来说计算就是get
和set
,下面我们就通过一个举例来说明一下计算属性。
示例代码:
class Teacher {
var age: Int {
get {
return 18
}
set {
age = newValue
}
}
}
let t = Teacher()
t.age = 10
print("end")
对于计算属性就是属性自己实现get
和set
方法,但是示例代码中这样写是不对的,首先会有警告:
⚠️警告的意思就是会自己调用自己,如果运行这段代码就会造成无限递归调用而崩溃:
2.1 计算属性的使用
在上面的介绍中,是我们通常对属性的get
和set
写法,既然这样不对,那么计算属性该怎么用呢?下面我们在来看一个正方形的示例。
实例代码:
class Square{
var width: Double = 10.0
var area: Double{
get{
//这里的return可以省略,编译器会自动推导
return width * width
}
set{
width = sqrt(newValue)
}
}
}
let s = Square()
s.width = 8.0
print(s.area)
s.area = 81.0
print(s.width)
print("end")
打印结果:
这里我们可以看到可以通过对计算属性的get
和set
方法内的一些操作,实现对其他属性的修改。
2.3 通过lldb查看计算属性的存储
这里的0x4024000000000000
就是Swift
中对Double
类型的存储,这里我们对width
的赋值为10。
2.2 通过SIL文件查看计算属性
按照上面所说的命令,我们在编译一个sil
文件,sil
代码如下:
2.2.1 类
class Square {
@_hasStorage @_hasInitialValue var width: Double { get set }
var area: Double { get set }
@objc deinit
init()
}
我们看到计算属性area
没有了@_hasStorage
的修饰。
2.2.2 get&set
get方法及分析:
// Square.area.getter
sil hidden @main.Square.area.getter : Swift.Double : $@convention(method) (@guaranteed Square) -> Double {
// %0 "self" // users: %5, %4, %3, %2, %1
bb0(%0 : $Square):
debug_value %0 : $Square, let, name "self", argno 1 // id: %1
// width get 方法的函数地址
%2 = class_method %0 : $Square, #Square.width!getter : (Square) -> () -> Double, $@convention(method) (@guaranteed Square) -> Double // user: %3
// 调用函数,参数是%0-self,返回值width
%3 = apply %2(%0) : $@convention(method) (@guaranteed Square) -> Double // user: %6
// width get 方法的函数地址
%4 = class_method %0 : $Square, #Square.width!getter : (Square) -> () -> Double, $@convention(method) (@guaranteed Square) -> Double // user: %5
// 调用函数,参数是%0-self,返回值width
%5 = apply %4(%0) : $@convention(method) (@guaranteed Square) -> Double // user: %7
// 转成 结构体
%6 = struct_extract %3 : $Double, #Double._value // user: %8
// 转成 结构体
%7 = struct_extract %5 : $Double, #Double._value // user: %8
// 计算,并将返回值存储到%8
%8 = builtin "fmul_FPIEEE64"(%6 : $Builtin.FPIEEE64, %7 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %9
// 将%8 的值包装到%9
%9 = struct $Double (%8 : $Builtin.FPIEEE64) // user: %10
// 返回
return %9 : $Double // id: %10
} // end sil function 'main.Square.area.getter : Swift.Double'
set方法及分析:
// Square.area.setter
sil hidden @main.Square.area.setter : Swift.Double : $@convention(method) (Double, @guaranteed Square) -> () {
// %0 "newValue" // users: %5, %2
// %1 "self" // users: %7, %6, %3
bb0(%0 : $Double, %1 : $Square):
debug_value %0 : $Double, let, name "newValue", argno 1 // id: %2
debug_value %1 : $Square, let, name "self", argno 2 // id: %3
// function_ref sqrt
// 获取sqrt函数的地址
%4 = function_ref @sqrt : $@convention(c) (Double) -> Double // user: %5
// 调用sqrt函数
%5 = apply %4(%0) : $@convention(c) (Double) -> Double // user: %7
// 获取width set 方法的函数地址
%6 = class_method %1 : $Square, #Square.width!setter : (Square) -> (Double) -> (), $@convention(method) (Double, @guaranteed Square) -> () // user: %7
// 调用width set 方法,传入参数%5---sqrt方法的返回值,%1---self
%7 = apply %6(%5, %1) : $@convention(method) (Double, @guaranteed Square) -> ()
%8 = tuple () // user: %9
return %8 : $() // id: %9
} // end sil function 'main.Square.area.setter : Swift.Double'
2.3 小结
打印内存占用大小:
这里的24 就是8+8+8得来的,所以可以得到一个结论就是计算属性不占用对象分配的内存空间。
综上所述:
- 计算属性的本质就是
get
和set
方法 - 计算属性不占用对象的内存空间
3. 延迟存储属性
延迟存储属性就是我们常说的懒加载,懒加载就是使用的时候在加载,那么我们就来看看Swift
中这个延迟是什么样的。举个例子:
class Teacher {
lazy var age:Int = 10
}
let t = Teacher()
print(t.age)
print("end")
3.1 通过lldb查看延迟存储属性在内存中的存储
延迟存储属性实际上还是存储属性,既然是存储属性肯定是占用对象分配的内存的,下面我们通过lldb
来看看延迟存储属性在内存中是怎样存储的。
lldb调试:
在这个截图中第一段x/8gx
打印的是第一个断点时的数据,第二个是打印的第二断点时的数据。
- 通过以上的数据我们可以看到,延迟存储属性在不使用的时候是没有存储值的,虽然在代码的初始化中我们给它赋了个初始值
10
- 当我们使用了这个属性后,在内存中打印的时候就可以在看到这个值出现在了内存段中,这里的
10
是就是16进制的0xa
3.2 通过sil代码进一步探索延迟存储属性
修改Swift
为如下,这里我们即使用set
也使用get
:
class Teacher {
lazy var age:Int = 10
}
let t = Teacher()
t.age = 18
print(t.age)
print("end")
使用如下命令将我们的main.swift
文件编译成sil
文件,这里还删除了原有的sil
文件,并通过xcrun swift-demangle
命令还原sil
代码中的混淆。
rm -rf main.sil && swiftc -emit-sil main.swift | xcrun swift-demangle >> ./main.sil && open main.sil
3.2.1 类
class Teacher {
lazy var age: Int { get set }
@_hasStorage @_hasInitialValue final var $__lazy_storage_$_age: Int? { get set }
@objc deinit
init()
}
我们可以看到在sil
代码中Teacher
这个类中的延迟存储属性:
- 有一个和
swift
代码中书写差不多的属性,带有get
和set
方法 - 在存储属性的基础上对
var
变量增加了final
修饰 - 生成了
$__lazy_storage_$_age
这个可选(optional
)变量
3.2.2 main 函数
针对main函数中的主要调用在截图中加了注释:
- 首先创建变量
t
的内存地址方到%3
- 获取
t
的metadata
- 获取
__allocating_init
的函数地址,用于对象的初始化 - 调用
__allocating_init
函数传入metadata
初始化变量t
- 将初始化的结果存如
%3
的地址中 - 将
%3
这个内存地址中的t对象的数据加载到%8
中 - 将要赋值的18这个值放到
%9
这个虚拟寄存器中 - 通过
%9
中的值初始化一个Int
类型的的结构体放到%10
中 -
age
属性的set
方法放到%11
中 - 调用
age
的set
方法,传入%10--18
和%8--self
- 同
%8
一致,将%3
这个内存地址中的t
对象的数据加载到%19
中 -
age
的get
方法放到%20
中 - 调用
age
的get
方法,传入%19--self
参数,并将返回值存储到%21
中
3.2.3 set方法
// Teacher.age.setter
sil hidden @main.Teacher.age.setter : Swift.Int : $@convention(method) (Int, @guaranteed Teacher) -> () {
// %0 "value" // users: %4, %2
// %1 "self" // users: %5, %3
bb0(%0 : $Int, %1 : $Teacher):
debug_value %0 : $Int, let, name "value", argno 1 // id: %2
debug_value %1 : $Teacher, let, name "self", argno 2 // id: %3
// 将传入的value值%0存储到%4中
%4 = enum $Optional<Int>, #Optional.some!enumelt, %0 : $Int // user: %7
// 获取懒加载的$__lazy_storage_$_age的地址存放到%5中
%5 = ref_element_addr %1 : $Teacher, #Teacher.$__lazy_storage_$_age // user: %6
// 开始访问age的内存
%6 = begin_access [modify] [dynamic] %5 : $*Optional<Int> // users: %7, %8
// 将%4的值存储到%6中
store %4 to %6 : $*Optional<Int> // id: %7
// 结束内存的访问
end_access %6 : $*Optional<Int> // id: %8
// 返回一个空的元组
%9 = tuple () // user: %10
return %9 : $() // id: %10
} // end sil function 'main.Teacher.age.setter : Swift.Int'
由sil
中的set
方法我们可以知道,在延迟存储属性中:
- 获取延迟属性的内存地址
- 将传入的值存储到内存地址中
3.2.4 get方法
// Teacher.age.getter
sil hidden [lazy_getter] [noinline] @main.Teacher.age.getter : Swift.Int : $@convention(method) (@guaranteed Teacher) -> Int {
// %0 "self" // users: %14, %2, %1
bb0(%0 : $Teacher):
debug_value %0 : $Teacher, let, name "self", argno 1 // id: %1
// 获取$__lazy_storage_$_age的内存地址存放到%2中
%2 = ref_element_addr %0 : $Teacher, #Teacher.$__lazy_storage_$_age // user: %3
// 开始访问内存
%3 = begin_access [read] [dynamic] %2 : $*Optional<Int> // users: %4, %5
// 加载内存中数据存放到%4中
%4 = load %3 : $*Optional<Int> // user: %6
// 结束内存访问
end_access %3 : $*Optional<Int> // id: %5
// 如果%4中有值(some)则跳转bb1,如果没有值(none)则跳转到bb2
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
// 跳转bb3
br bb3(%7 : $Int) // id: %9
bb2: // Preds: bb0
// 将给age的初始值10取出存放到%10中
%10 = integer_literal $Builtin.Int64, 10 // user: %11
// 用一个Int类型的结构体包装这个10存放到%11
%11 = struct $Int (%10 : $Builtin.Int64) // users: %18, %13, %12
debug_value %11 : $Int, let, name "tmp2" // id: %12
// 判断self是否为空,不空则将%11的值存放到%13中,否则$Int
%13 = enum $Optional<Int>, #Optional.some!enumelt, %11 : $Int // user: %16
// 获取$__lazy_storage_$_age的内存地址存放到%14中
%14 = ref_element_addr %0 : $Teacher, #Teacher.$__lazy_storage_$_age // user: %15
// 开始访问内存
%15 = begin_access [modify] [dynamic] %14 : $*Optional<Int> // users: %16, %17
// 将%13中的值存储到%15
store %13 to %15 : $*Optional<Int> // id: %16
// 结束内存访问
end_access %15 : $*Optional<Int> // id: %17
// 跳转bb3
br bb3(%11 : $Int) // id: %18
// %19 // user: %20
bb3(%19 : $Int): // Preds: bb2 bb1
// 返回传入的值%19,如果%19为空则返回$Int
return %19 : $Int // id: %20
} // end sil function 'main.Teacher.age.getter : Swift.Int'
由sil
中的get
方法我们可以知道,在延迟存储属性中:
- 获取延迟属性的内存地址
- 然后取出该地址中的值
- 如果获取的值为空则将默认值取出并返回
- 如果不为空则直接返回取的值
通过该sil
代码我们可以看出,延迟存储属性的get
是线程不安全的,为什么不安全呢?
- 如果线程1调用
get
,执行完bb0
,判断age
没有值,将会执行bb2
- 此时正好
cpu
时间片分配给了线程2,线程2也访问age
,此时age
依然没有值,同样会走bb2
给age
赋初始值 - 待线程2执行完毕后线程1又获得执行权限,同样开始执行
bb2
,又会给age
赋初值值 - 以上并不会影响
age
的值,如果线程2调用的是set
此时修改了age
的值,然后线程1在执行bb2
的时候,给age
赋初始值,就会因线程问题导致我们age
的值不准确
3.3 内存的占用
打印非延迟属性和延迟属性类的内存的占用大小
可以看到在使用lazy
修饰的延迟存储属性在对象的内存占用上会比不使用lazy
修饰的多,那么这是为什么呢?
- 我们在上面分析
sil
代码的时候知道lazy
修饰的属性在底层是可选类型,在这里实际就是Optional<Int>
。 - 不使用
lazy
修饰的时候,对象的内存占用是24,是由metadat
+refCounts
+Int
得来的 - 使用
lazy
修饰的时候,对象的内存占用是32是由metadat
+refCounts
+Optional<Int>
得来的
这里我们就来看看Optional<Int>
的内存占用:
可以看到Optional<Int>
的size是9,其实这里是Int
的8,加上Optional
的1,Optional
在底层是个枚举,默认是Int8
占用1字节,在后续会详细说明。由于需要内存对齐,都是8字节8字节的读取,所以剩下不用的也要补齐。
3.4 lazy的其他用法
let data = 0...3
let result = data.lazy.map { (i: Int) -> Int in
print("Handling...")
return i * 2
}
print("Begin:")
for i in result {
print(i)
}
在 Swift 标准库中,也有一些 Lazy 方法,就可以在不需要运行时,避免消耗太多的性能。
打印结果如下:
3.5 小结
经过上面的分析现总结如下:
- 延迟存储属性需要使用
lazy
修饰 - 延迟存储属性必须有一个默认的初始值
- 延迟存储属性在第一次访问的时候才能被赋值
- 延迟存储属性并不保证线程安全
- 延迟存储属性对实例对象的大小有影响
4. 类型属性
类型属性跟类方法的命名类似,是属于类的一个属性,不能通过实例对象去访问,类型属性需要使用static
进行修饰,举个例子:
class Teacher {
static var age:Int = 18
}
let t = Teacher()
Teacher.age = 20
print(Teacher.age)
print("end")
4.1 通过lldb对类型属性初步探索
类型属性也是属性,那么类型属性会存储在哪里呢?下面我们通过lldb
调试,分别从对象、类、类型属性的内存来对类型属性进行初步探索。
我们首先打印了对象t
,查看对象的内存,并没有找到对象中对age
的存储。
随后我们查看对象的metadata
,也并没有相关属性。
我们又打印了Teacher.age
,得到的直接是age
的值
那么类型属性到底存储在哪里呢?下面我们通过sil
代码来看看。
4.2 通过sil进一步探索类型属性
生成sil
代码的方式见3.2
这里为了简化sil
代码,将Swift
代码修改成如下:
class Teacher {
static var age:Int = 18
}
Teacher.age = 20
4.2.1 类
class Teacher {
@_hasStorage @_hasInitialValue static var age: Int { get set }
@objc deinit
init()
}
在类中主要的区别就是:相对于普通的存储属性增加了static
修饰。
在这里我们并没有得出什么有用的结论,下面我们继续往下看:
此时我们看到如上图所示的sil_global
,这里我们的Teacher.age
已经是一个全局变量了。下面我们就去main
函数中看看age
的初始化。
4.2.2 main函数
// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
%2 = metatype $@thick Teacher.Type
// function_ref Teacher.age.unsafeMutableAddressor
%3 = function_ref @main.Teacher.age.unsafeMutableAddressor : Swift.Int : $@convention(thin) () -> Builtin.RawPointer // user: %4
// 调用%3中存储的函数,按照命名应该是获取Teacher.age的内存地址,并将返回结果存储到%4
%4 = apply %3() : $@convention(thin) () -> Builtin.RawPointer // user: %5
// 将%4的地址存放到%5
%5 = pointer_to_address %4 : $Builtin.RawPointer to [strict] $*Int // user: %8
// 构建20这个值
%6 = integer_literal $Builtin.Int64, 20 // user: %7
// 构建Int结构体
%7 = struct $Int (%6 : $Builtin.Int64) // user: %9
// 开始方法内存,%5中的,也就是age的地址
%8 = begin_access [modify] [dynamic] %5 : $*Int // users: %9, %10
// 将%7存储到%8中,也就是将20存储到age的地址
store %7 to %8 : $*Int // id: %9
// 结束内存访问
end_access %8 : $*Int // id: %10
%11 = integer_literal $Builtin.Int32, 0 // user: %12
%12 = struct $Int32 (%11 : $Builtin.Int32) // user: %13
return %12 : $Int32 // id: %13
} // end sil function 'main'
经过对main
函数的分析,我们可以知道:
- 类型属性的内存地址是通过
xxx.unsafeMutableAddressor
函数获取到的 - 然后将需要存储的值存储到这个内存中
- 这也就很好的解释了我们在
lldb
调试的时候为什么打印出来的是存储的值。
4.2.3 xxx.unsafeMutableAddressor
// Teacher.age.unsafeMutableAddressor
sil hidden [global_init] @main.Teacher.age.unsafeMutableAddressor : Swift.Int : $@convention(thin) () -> Builtin.RawPointer {
bb0:
// 全局地址
%0 = global_addr @globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_token0 : $*Builtin.Word // user: %1
// 全局地址存放到%1
%1 = address_to_pointer %0 : $*Builtin.Word to $Builtin.RawPointer // user: %3
// function_ref globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_func0
// 函数地址存储到%2
%2 = function_ref @globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_func0 : $@convention(c) () -> () // user: %3
// once 保证全局地址只被初始化一次
%3 = builtin "once"(%1 : $Builtin.RawPointer, %2 : $@convention(c) () -> ()) : $()
// 初始化的全局地址存放到%4
%4 = global_addr @static main.Teacher.age : Swift.Int : $*Int // user: %5
// 地址转为指针存放到%5
%5 = address_to_pointer %4 : $*Int to $Builtin.RawPointer // user: %6
// 返回
return %5 : $Builtin.RawPointer // id: %6
} // end sil function 'main.Teacher.age.unsafeMutableAddressor : Swift.Int'
我们可以在这个函数中看到:
- 通过
@globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_token0
创建一个全局地址,这个就是类下面那段代码 - 这里面还用到了
builtin "once"
来保证值初始化一次
4.2.4 builtin "once"
那么这个builtin "once"
是怎么保证类型属性只初始化一次的呢?
我们通过添加断点和汇编调试来看看。
添加断点:
开启汇编调试:
汇编代码:
在汇编代码中我们看到断点在c
处,我们按住command
点击鼠标,跳转到下一层中:
我们看到这里调用的是swift_once
我们在swift_once
这行添加断点,过掉上一个断点,command
点击鼠标,跳转到下一层中:
此时我们看到了dispatch_once_f
,这里基本就能证明了builtin "once"
实际上还是使用了GCD
中的dispatch_once
来保证类型属性只被初始化一次的。
其实我们来到Swift
源码中,搜索一下swift_once
/// Runs the given function with the given context argument exactly once.
/// The predicate argument must point to a global or static variable of static
/// extent of type swift_once_t.
void swift::swift_once(swift_once_t *predicate, void (*fn)(void *),
void *context) {
#if 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
}
通过对源码的探索,我们也可以清楚的看到,swift_once
在apple
平台调用的是dispatch_once
。
4.2.5 get&set
下面我们在来看看类型属性的get
和set
方法
get方法及分析:
// static Teacher.age.getter
sil hidden [transparent] @static main.Teacher.age.getter : Swift.Int : $@convention(method) (@thick Teacher.Type) -> Int {
// %0 "self" // user: %1
bb0(%0 : $@thick Teacher.Type):
debug_value %0 : $@thick Teacher.Type, let, name "self", argno 1 // id: %1
// function_ref Teacher.age.unsafeMutableAddressor
// 将获取地址的函数存储到%2
%2 = function_ref @main.Teacher.age.unsafeMutableAddressor : Swift.Int : $@convention(thin) () -> Builtin.RawPointer // user: %3
// 调用函数并将返回结果存储到%3
%3 = apply %2() : $@convention(thin) () -> Builtin.RawPointer // user: %4
// 指针转地址存放到%4
%4 = pointer_to_address %3 : $Builtin.RawPointer to [strict] $*Int // user: %5
// 开始内存访问
%5 = begin_access [read] [dynamic] %4 : $*Int // users: %6, %7
// 加载内存地址中的数据存储到%6
%6 = load %5 : $*Int // user: %8
// 结束内存访问
end_access %5 : $*Int // id: %7
// 返回取到的数据
return %6 : $Int // id: %8
} // end sil function 'static main.Teacher.age.getter : Swift.Int'
set方法及分析:
// static Teacher.age.setter
sil hidden [transparent] @static main.Teacher.age.setter : Swift.Int : $@convention(method) (Int, @thick Teacher.Type) -> () {
// %0 "value" // users: %8, %2
// %1 "self" // user: %3
bb0(%0 : $Int, %1 : $@thick Teacher.Type):
debug_value %0 : $Int, let, name "value", argno 1 // id: %2
debug_value %1 : $@thick Teacher.Type, let, name "self", argno 2 // id: %3
// function_ref Teacher.age.unsafeMutableAddressor
// 获取age的地址的函数存放到%4
%4 = function_ref @main.Teacher.age.unsafeMutableAddressor : Swift.Int : $@convention(thin) () -> Builtin.RawPointer // user: %5
// 调用函数并将返回值存放到%5
%5 = apply %4() : $@convention(thin) () -> Builtin.RawPointer // user: %6
// 指针转地址存储到%6
%6 = pointer_to_address %5 : $Builtin.RawPointer to [strict] $*Int // user: %7
// 开始方法指针
%7 = begin_access [modify] [dynamic] %6 : $*Int // users: %8, %9
// 将传入的参数%0 存储到%7
store %0 to %7 : $*Int // id: %8
// 结束内存访问
end_access %7 : $*Int // id: %9
%10 = tuple () // user: %11
return %10 : $() // id: %11
} // end sil function 'static main.Teacher.age.setter : Swift.Int'
4.3 类型属性的内存占用
通过上面的分析,我们可以知道类型属性是一个全局变量,那么类型属性是不是不会占用类的存储空间呢?下面我们打印来看看:
通过打印我们可以知道,类型属性并不会占用类的空间。
4.4 单例的创建
回想一下,在Objective-C
我们会使用如下的方式创建一个单例:
@implementation SITeacher
+ (instancetype)shareInstance{
static SITeacher *shareInstance = nil;
dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
shareInstance = [[SITeacher alloc] init];
});
return shareInstance;
}
@end
既然类型存储属性是个全局变量,那么我们在Swift
中创建一个单例就简单多了,代码如下:
class Teacher {
// 声明一个不可变的 let 类型 static 属性
// 为当前类的实例对象
static let shared = Teacher()
// 将当前类的 init 方法 添加private访问权限
private init(){}
}
其实经常写Swift
代码的人都知道Swift
中单例是怎么写的,但是其原理是什么并不是很多人都知道的,其实就是类型属性是个全局变量,而且只初始化一次,符合单例的条件。
4.5 小结
通过上面的分析,我们总结如下:
- 类型属性通过
static
来修饰 - 类型属性需要附一个默认的初始值
- 类型属性只会被初始化一次
- 类型属性是线程安全的
- 类型属性存储为全局变量
- 类型属性不占用对象的存储空间
- 类型属性可以用作单例来使用,前提是需要
private
类的init
方法。
5. 属性观察者
介绍了完了Swift
中的几种属性后,下面我们来看看在属性中使用频率最高的属性观察者。属性观察者在代码中实际就是willSet
和didSet
class Teacher {
var age:Int = 18 {
//赋新值之前调用
willSet {
print("willSet newValue --- \(newValue)")
}
// 赋新值之后调用
didSet {
print("didSet oldValue --- \(oldValue)")
}
}
}
let t = Teacher()
t.age = 20
print("end")
打印结果:
通过打印结果我们可以看到,在赋新值前我们可以拿到即将要赋予的新值,在赋新值后也能够拿到赋值前的旧值。
5.2 通过sil探索属性观察者
sil
编译命令详见3.2
5.2.1 类
class Teacher {
@_hasStorage @_hasInitialValue var age: Int { get set }
@objc deinit
init()
}
在类中相比于普通的存储属性没有什么区别。
5.2.2 set 方法
其实属性观察主要是在设置新值的时候触发的,这里我们直接来看set
方法。
set方法及分析:
// Teacher.age.setter
sil hidden @main.Teacher.age.setter : Swift.Int : $@convention(method) (Int, @guaranteed Teacher) -> () {
// %0 "value" // users: %13, %10, %2
// %1 "self" // users: %16, %11, %10, %4, %3
bb0(%0 : $Int, %1 : $Teacher):
debug_value %0 : $Int, let, name "value", argno 1 // id: %2
debug_value %1 : $Teacher, let, name "self", argno 2 // id: %3
// 找出实例内部物理实例变量的地址
%4 = ref_element_addr %1 : $Teacher, #Teacher.age // user: %5
// 开始访问内存
%5 = begin_access [read] [dynamic] %4 : $*Int // users: %6, %7
// 将内存中的值加载到%6
%6 = load %5 : $*Int // users: %8, %16
// 结束内存访问
end_access %5 : $*Int // id: %7
debug_value %6 : $Int, let, name "tmp" // id: %8
// function_ref Teacher.age.willset
// 获取 willset 的函数地址
%9 = function_ref @main.Teacher.age.willset : Swift.Int : $@convention(method) (Int, @guaranteed Teacher) -> () // user: %10
// 调用 willset 函数,传入%0---新值,%1---self
%10 = apply %9(%0, %1) : $@convention(method) (Int, @guaranteed Teacher) -> ()
// 找出实例内部物理实例变量的地址
%11 = ref_element_addr %1 : $Teacher, #Teacher.age // user: %12
// 开始访问内存
%12 = begin_access [modify] [dynamic] %11 : $*Int // users: %13, %14
// 存储新值
store %0 to %12 : $*Int // id: %13
// 结束内存访问
end_access %12 : $*Int // id: %14
// function_ref Teacher.age.didset
// 获取 didset 的函数地址
%15 = function_ref @main.Teacher.age.didset : Swift.Int : $@convention(method) (Int, @guaranteed Teacher) -> () // user: %16
// 调用 didset 传入参数 %6---旧值,%1---self
%16 = apply %15(%6, %1) : $@convention(method) (Int, @guaranteed Teacher) -> ()
%17 = tuple () // user: %18
return %17 : $() // id: %18
} // end sil function 'main.Teacher.age.setter : Swift.Int'
从set
方法中我们可以看到,使用了属性观察者的属性,在set
方法中:
- 会将属性中的旧值取出,存起来
- 在设置新值前会调用
willSet
函数,并将新值作为参数传入 - 在设置新值后会调用
didSet
函数,并将旧值作为参数传入
5.2.3 willSet&didSet
在 willSet
函数中我们可以看到,其内部声明了newValue
变量,所以我们可以直接使用
在 didSet
函数中我们同样也看到了oldValue
变量的声明,同样也为我们直接使用提供了变量。
5.3 属性观察者的使用
5.3.1 init方法中会触发属性观察者吗
class Teacher {
var age: Int = 18 {
//赋新值之前调用
willSet {
print("willSet newValue --- \(newValue)")
}
// 赋新值之后调用
didSet {
print("didSet oldValue --- \(oldValue)")
}
}
init() {
age = 20
}
}
print("end")
运行结果:
我们可以看到在init
方法中给属性赋值,并没有触发属性观察者,那是为什么呢?
init
方法会初始化当前变量,对于存储属性,在分配内存后,会调用memset
清理内存,因为可能存在脏数据,清理完才会赋值。
还有一种原因就是,我们如果有多个属性,我们给A属性赋值,并在A属性的观察者中访问了B属性,此时B属性可能还没有被初始化,此时我们就有可能获取到脏数据,导致结果不准确。
(PS:)仔细想想也不是很严谨,所以通过sil
代码来看看,调用流程如下:
@main.Teacher.__allocating_init()
——>@main.Teacher.init()
// Teacher.init()
sil hidden @main.Teacher.init() -> main.Teacher : $@convention(method) (@owned Teacher) -> @owned Teacher {
// %0 "self" // users: %14, %10, %4, %1
bb0(%0 : $Teacher):
debug_value %0 : $Teacher, let, name "self", argno 1 // id: %1
%2 = integer_literal $Builtin.Int64, 18 // user: %3
%3 = struct $Int (%2 : $Builtin.Int64) // user: %6
%4 = ref_element_addr %0 : $Teacher, #Teacher.age // user: %5
%5 = begin_access [modify] [dynamic] %4 : $*Int // users: %6, %7
store %3 to %5 : $*Int // id: %6
end_access %5 : $*Int // id: %7
%8 = integer_literal $Builtin.Int64, 20 // user: %9
%9 = struct $Int (%8 : $Builtin.Int64) // user: %12
%10 = ref_element_addr %0 : $Teacher, #Teacher.age // user: %11
%11 = begin_access [modify] [dynamic] %10 : $*Int // users: %12, %13
store %9 to %11 : $*Int // id: %12
end_access %11 : $*Int // id: %13
return %0 : $Teacher // id: %14
} // end sil function 'main.Teacher.init() -> main.Teacher'
在init
方法中,我们可以看到,属性的赋值都是操作的属性的内存地址,并没有调用willSet
和didSet
相关方法。但是赋值也是在属性初始化之后,所以对于memset
应该是站不住脚的,对于属性观察者中调用其他存储属性按道理也是站不住脚的。
下面我们换一下测试代码:
class Teacher {
var age2: Int {
get{
return age
}
set{
self.age = newValue
}
}
var age: Int = 18 {
//赋新值之前调用
willSet {
print("willSet newValue --- \(newValue)")
}
// 赋新值之后调用
didSet {
print("didSet oldValue --- \(oldValue)")
}
}
init() {
age2 = 20
}
}
打印结果:
此时我们发现在计算属性中给存储属性赋值就会调用属性观察者,下面我们通过sil
代码看看调用。
init 函数:
我们可以看到在init
函数中对于计算属性是直接调用了其setter
方法。并且是在存储属性age
初始化完毕后才调用的计算属性的setter
。
我们可以看到在计算属性的的setter
方法中给存储属性赋值是调用的存储属性的setter
方法,在上面的分析中我们已经知道了,在存储属性的setter
方法中会调用willSet
和didSet
。
综上所述,在init
方法中给存储属性赋值是否触发属性观察者应该是这样的:
- 赋值前后自己是能知道的,没必要在调用属性观察者
- 在计算属性中给存储属性赋值,是因为计算属性的代码一般是固定的,并且这个值有可能计算后的,所以需要触发属性观察者,使其做后续处理
5.3.2 在那些地方可以添加属性观察者
- 属性观察者应用在存储属性上
- 计算属性自己实现了
get
和set
,此时要想做些什么事情,可以随便去做,不需要属性观察者 - 但是继承的计算属性也是可以添加属性观察者的
应用在存储属性上在上面一直在使用,这里就不验证了
对于计算属性,验证结果如下:
我们可以看到willSet
和didSet
不能和getter
一起提供,那么我们注释掉getter
呢?
此时我们看到有setter
的变量也必须有getter
所以属性观察者不能添加到计算属性中
下面我们来验证在继承的计算属性中的属性观察者:
我们看到对于继承计算属性的同样可以添加属性观察者,并在赋值的时候调用。
5.3.3 属性观察者的调用顺序
如果是继承存储属性,并且父类和子类都添加了属性观察者,那么这个调用顺序是什么呢?,验证如下:
我们看到调用顺序是:
- 先调用子类的
willSet
,再调用父类的willSet
- 先调用父类的
didSet
,在调用子类的didSet
其实也很好理解,首先通知子类要改变,毕竟是给子类赋值,然后在通知父类改变,父类改变完了先在父类做些处理,然后在让子类改变,子类在做最后的处理。
5.3.4 在子类的init方法中给属性赋值
如果在子类的init方法中给属性赋值会怎样调用属性观察者呢?测试结果如下:
我们可以看到在子类的init
方法中给属性赋值与直接给子类对象赋值的调用属性是一模模一样样的。我的理解是这样的:
- 在子类的
init
方法中赋值,父类是监听不到,所以需要触发父类的属性观察者,使其作出相应处理 - 当父类做完处理子类也是监听不到的,所以同意需要触发子类属性观察者通知子类作出相应处理
6. Then
在Swift
中对于属性也会经常用到一个叫Then
的框架:
其用法如下:
6.1 Then
import Then
let label = UILabel().then {
$0.textAlignment = .center
$0.textColor = .black
$0.text = "Hello, World!"
}
以上等效于
let label: UILabel = {
let label = UILabel()
label.textAlignment = .center
label.textColor = .black
label.text = "Hello, World!"
return label
}()
6.2 with
let newFrame = oldFrame.with {
$0.size.width = 200
$0.size.height = 100
}
newFrame.width // 200
newFrame.height // 100
6.3 do
UserDefaults.standard.do {
$0.set("devxoul", forKey: "username")
$0.set("devxoul@gmail.com", forKey: "email")
$0.synchronize()
}
其原理其实也蛮简单的,感兴趣的可以去看看。
7. 总结
至此我们对Swift
中的属性基本就分析完毕了先在总结如下:
存储属性:
- 存储属性是占用实例存储空间的
- 在
sil
代码中使用``@_hasStorage关键字
修饰 - 再其
get
和set
方法中通过访问内存获取和修改属性值
计算属性:
- 计算属性的本质就是
get
和set
方法 - 计算属性不占用实例对象的内存空间
延迟存储属性:
- 延迟存储属性也是存储属性
- 延迟存储属性需要使用
lazy
修饰 - 延迟存储属性必须有一个默认的初始值
- 延迟存储属性在第一次访问的时候才能被赋值
- 延迟存储属性并不保证线程安全
- 延迟存储属性对实例对象的大小有影响,一般会增加内存占用
类型属性:
- 类型属性通过
static
来修饰 - 类型属性需要赋一个默认的初始值
- 类型属性只会被初始化一次
- 类型属性是线程安全的
- 类型属性存储为全局变量
- 类型属性不占用对象的存储空间
- 类型属性可以用作单例来使用,前提是需要
private
类的init
方法。
属性观察者:
- 属性观察者是观察属性改变前后的,本质就是
willSet
和didSet
- 属性观察者可以添加在
- 存储属性
- 继承的存储属性
- 继承的计算属性
- 如果并没有在属性中添加属性观察者,则不会调用属性观察者
- 在底层代码中会根据开发者对属性观察者的依赖调用属性观察者