14 KiB
属性
计算属性的本质是方法。
struct Circle {
var radius: Int
var diameter: Int {
set {
radius = newValue/2
}
get {
2 * radius
}
}
}
var circle = Circle(radius: 10)
// print(circle.diameter) // 20
circle.diameter = 24
// print(circle.radius) // 12
let diameter = circle.diameter
计算属性 y 等价于下面的代码:
setDiameter (newValue: Int) {
radius = newValue/2
}
getDiameter () {
return 2*radius
}
然后通过汇编来窥探下,在 circle.diameter = 22 处加断点,可以看到本质上调用的就是 setter 方法,setter 内部的实现就不一一窥探了
然后断点继续,在 let diameter = circle.diameter 处加断点,可以看到调用了 getter 方法
异同点
存储属性:
-
类似于成员变量
-
存储在实例的内存中
-
结构体、类可以定义存储属性
-
枚举不可以定义存储属性
-
在创建类、结构体的实例时,必须为所有的存储属性设置一个合适的初始值
-
延迟存储属性必须是
var,不能是let。 因为 Swift 规定 let 必须在实例的初始化方法完成之前就拥有值 -
lazy在多线程情况下,无法保证属性只被初始化1次。struct Point { var x:Int lazy var y = 0 init(_ x: Int = 0) { self.x = x } } var p = Point(2) print(p.y) -
当结构体包含一个延迟存储属性时,只有 var 才能访问延迟存储属性(因为延迟属性初始化的时候需要改变结构体内存)。Class 的话,实例可以用 let 修饰,访问延迟存储属性是可以的。
QA:为什么结构体包含一个延迟存储属性时,只有 var 才能访问延迟存储属性?
之前在 Swift 结构体和类的内存布局 探究过
struct的内存布局,struct的成员变量在内存中是连续存储的。由于是延迟存储属性,等真正使用的时候才会执行延迟属性的初始化逻辑,才会更改struct的内存,所以let无法满足更改内存的需求。struct Point { var x = 0 lazy var y = 0 } let p = Point() print(p.y) // Cannot use mutating getter on immutable value: 'p' is a 'let' constant var p2 = Point() print(p2.y) // 0 class Point { var x:Int lazy var y = 0 init(_ x: Int = 0) { self.x = x } } let p = Point(2) print(p.y) // 0
计算属性:
- 本质就是方法
- 不占用实例内存
- 枚举、结构体、类都可以定义计算属性
- 计算属性只能用 var,不能用 let
枚举 rawValue 的原理
枚举原始值 rawValue 的本质:只读的计算属性,不占用实例内存。
enum Season: Int {
case spring = 10
case summer = 20
case autumn = 30
case winter = 40
}
let season = Season.summer
// season.rawValue = 22 // Cannot assign to property: 'rawValue' is immutable
print(season.rawValue)
通过汇编可以可以看到,在调用 enum 的 rawvalue 的时候本质是通过计算属性调用 getter 来实现的。
属性观察器
-
可以为非
lazy的var存储属性设置属性观察期 -
在初始化器中设置属性值不会触发
willSet、didSet -
属性观察器、计算属性的功能,同样可以应用在全局变量、局部变量上
var num: Int { get { return 10 } set { print("newValue", newValue) } } num = 11 print(num) // console newValue 11 10 func test () { var age: Int { set { print("new age is ", newValue) } get { 28 } } age = 29 print(age) } test() // console new age is 29 28
Inout 核心原理
普通的存储属性
struct Shape {
var width: Int
var side: Int {
willSet {
print("willset side", newValue)
}
didSet {
print("didset side", oldValue, side)
}
}
var girth: Int {
set {
width = newValue/side
print("set girth ", newValue)
}
get {
print("get girth")
return width * side
}
}
func show() {
print("width is \(width), side is \(side), girth is \(girth)")
}
}
func changeValue(_ value: inout Int) {
value = 20
}
var shape = Shape(width: 10, side: 4)
changeValue(&shape.width)
shape.show()
// console
get girth
width is 20, side is 4, girth is 80
在 changeValue(&shape.width) 处加汇编可以看到断点停在第10行 leaq 0x953c(%rip), %rdi 即将 rip + 0x953c = 0x100002cbc + 0x953c = 0x10000C1F8 赋值给 rdi。
第16行也是一样,leaq 0x9523(%rip), %rdi 即将 rip + 0x9523 = 0x100002cd5 + 0x953c = 0x10000C1F8 赋值给 rdi。
然后看到17行的关键代码,LLDB 输入 si,可以看到在第6行 movq $0x14, (%rdi),将16进制的 0x14 也就是20,移动到指定的内存地址 rdi 上
因为 struct 结构体内存布局中,成员变量在内存中是连续存储的。所以 struct 的地址也就是 struct 中第一个成员变量 width 的地址。
总结:普通的存储属性,在调用方法的时候,如果参数是 inout 传递引用,则直接传递存储属性的地址值即可。在方法内部,对该地址对应的值进行修改即可。
计算属性
对调用的代码进行调整
var shape = Shape(width: 10, side: 4)
changeValue(&shape.girth)
shape.show()
// console
get girth
set girth 20
get girth
width is 5, side is 4, girth is 20
在 changeValue(&shape.girth) 处下断点,查看汇编
核心思路:方法参数用 inout修饰,则传递的是引用(内存地址)。
-
汇编19行
callq 0x1000030e0 ; SwiftDemo.Shape.girth.getter : Swift.Int at main.swift:16调用了girth计算属性的getter,getter的返回值存放在寄存器rax上 -
20行将
movq %rax, -0x28(%rbp)函数返回值rax存放在main函数的栈空间上(虽然没有写调用函数,但是代码主入口就是main函数),且rbp到rsp之间都是函数的栈空间 -
21行
leaq -0x28(%rbp), %rdi将栈空间上-0x28(%rbp)的地址值赋值给rdi寄存器 -
22行
callq 0x1000037b0 ; SwiftDemo.changeValue(inout Swift.Int) -> () at main.swift:26调用changeValue方法,参数通过寄存器rdi传递,里面是栈空间 getter 值的地址。 -
LLDB 输入
si查看 changeValue 内部。可以看到第6行movq $0x14, (%rdi)直接将20赋值给rdi
-
LLDB 输入
finish结束changeValue细节,查看外部23行汇编movq -0x28(%rbp), %rdi,将main函数栈空间上getter返回值的内存对应的值,保存到寄存器rdi上。 -
25行
callq 0x100003250 ; SwiftDemo.Shape.girth.setter : Swift.Int at main.swift:12调用计算属性的setter,函数参数为rid寄存器里的值(也就是20)
总结:带有计算属性的存储属性,如果调用的方法参数是 inout 类型,系统真正实现,不能直接将对应的地址传进去,因为传进去很多简单,满足了修改值的需求。但是属性观察器的 set、get 就没办法触发了。所以为了触发属性观察器系统的设计是:
- 第一步:先将传递进去的属性调用
getter,保存在函数的栈地址空间内的某个内存上 - 第二步:然后调用带有
inout属性的方法,将步骤一得到的内存传递当作参数传进去。修改该内存对应的值 - 第三步:将步骤二得到的值后,调用
setter方法。
这个流程下来,满足了修改值,且触发了原始属性观察器的需求。
带有属性观察器的存储属性
对调用的代码进行调整
var shape = Shape(width: 10, side: 4)
changeValue(&shape.side)
shape.show()
// console
willset side 20
didset side 4 20
get girth
width is 10, side is 20, girth is 200
在 changeValue(&shape.side) 处添加断点,查看汇编
分析:
-
17行
movq 0x9549(%rip), %rax ; SwiftDemo.shape : SwiftDemo.Shape + 8将地址格式为0x9549(%rip)一个全局变量,也就是shape的地址 + 8 的值,赋值给rax寄存器 -
18行
movq %rax, -0x28(%rbp)将寄存器rax里的值,赋值给main函数的栈空间上的局部变量-0x28(%rbp) -
19行 将
main函数的栈空间上的局部变量-0x28(%rbp)的地址值,赋值给寄存器rdi -
20行
callq 0x1000037b0 ; SwiftDemo.changeValue(inout Swift.Int) -> () at main.swift:26调用changeValue方法。函数参数通过寄存器rdi传递,函数内部修改了该内存上的值 -
21行
movq -0x28(%rbp), %rdi将main函数的栈空间上的局部变量-0x28(%rbp)的值,赋值给寄存器rdi。也就是修改后的20 -
23行
callq 0x100002db0 ; SwiftDemo.Shape.side.setter : Swift.Int at main.swift:3调用 setter -
LLDB 输入
si,可以看到setter方法内部,调用了willSet、didSet
总结:带有属性观察器的存储属性,如果调用的方法参数是 inout 类型,系统真正实现,不能直接将对应的地址传进去,因为传进去很多简单,满足了修改值的需求。但是属性观察器的 willSet、didSet 就没办法触发了。所以为了触发属性观察器系统的设计是:
- 第一步:先将传递进去的地址,保存在函数的栈地址空间内的某个内存上
- 第二步:然后调用带有
inout属性的方法,将步骤一得到的内存传递当作参数传进去。修改该内存对应的值 - 第三步:将步骤二得到的值后,调用一个
setter方法,setter方法内,先调用willSet,再修改真正的带有观察属性的存储属性的值,最后调用didSet方法。
这个流程下来,满足了修改值,且触发了原始属性观察器的需求。
总结
-
如果实参有内存地址,且没有设置属性观察器和计算属性,实现是直接将实参的内存地址传入带
inout函数 -
如果实参是计算属性或者设置了属性观察器:系统采用了 Copy In Copy Out 的策略。
- 调用带
inout函数时,先复制实参的值,产生副本 (getter,栈空间上的局部变量) - 将副本的内存地址传入带
inout函数(副本进行引用传递),在函数内部修改副本的值 - 函数返回后,再将副本的值覆盖实参的值(setter,willSet、set)
- 调用带
类型属性
-
不同于存储属性,类型属性必须设置初始值,永伟类型属性不像存储属性那样有
init初始化器来初始化存储属性 -
类型属性就不存储在每个实例的内存里
-
存储属性默认就是
lazy,会在第一次使用的时候才初始化 -
存储属性就算被多个线程同时访问,但系统会保证只初始化1次
-
存储类型属性可以是
let -
存储属性可以用
class、static修饰 -
枚举 enum 里面不可以实例存储属性,但是可以定义类型存储属性
enum Season { static let age: Int = 0 case spring, summer, antumn, winter } var season = Season.summer -
类型属性的经典场景就是单例模式
class FileManager { private init() {} public static let sharedInstance: FileManager = FileManager() } var manager1 = FileManager.sharedInstance var manager2 = FileManager.sharedInstance var manager3 = FileManager.sharedInstance print(Mems.ptr(ofRef: manager1)) // 0x0000600000008030 print(Mems.ptr(ofRef: manager2)) // 0x0000600000008030 print(Mems.ptr(ofRef: manager3)) // 0x0000600000008030
Demo1:
movq $0xa, 0x86d1(%rip) num1 的地址为: rip + 0x86d1 = 0x100003b0f + 0x86d1 = 0x10000C1E0
movq $0xb, 0x86ce(%rip) num2 的地址为: rip + 0x86ce = 0x100003b1a + 0x86ce = 0x10000C1E8
movq $0xc, 0x86cb(%rip) num3 的地址为: rip + 0x86cb = 0x100003b25 + 0x86cb = 0x10000C1F0
可以看到 0x10000C1E0 0x10000C1E8 0x10000C1F0 在内存上是连续的,间隔8Byte。可见分配的3个全局变量内存是连续的
Demo2
movq $0xa, 0x8b65(%rip) num1 的内存为 rip + 0x8b65 = 0x1000037c3 + 0x8b65 = 0x10000C328
可以看到15行将11赋值给 rax,所以直接读取 rax 的地址:0x000000010000c330
movq $0xc, 0x8b38(%rip) num1 的内存为 rip + 0x8b38 = 0x100003800 + 0x8b38 = 0x10000C338
可以看到 0x10000C328 0x10000c330 0x10000C338 也是内存连续的。所以类型属性就是带有访问控制(必须通过类来访问)的全局变量
Demo3
类型属性如何保证线程安全的?如何保证只会初始化一次。
底层会调用 swift_once 进而调用 dispatch_once_t,dispatch_once_t 会传递一个函数地址进去执行,类型属性的初始化代码将会被包装成一个函数。 由 dispatch_once_t 保证线程安全和只初始化1次。