mirror of
https://github.com/NohamR/knowledge-kit.git
synced 2026-05-25 12:27:15 +00:00
434 lines
14 KiB
Markdown
434 lines
14 KiB
Markdown
# 属性
|
||
|
||
|
||
|
||
计算属性的本质是方法。
|
||
|
||
```swift
|
||
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` 等价于下面的代码:
|
||
|
||
```swift
|
||
setDiameter (newValue: Int) {
|
||
radius = newValue/2
|
||
}
|
||
getDiameter () {
|
||
return 2*radius
|
||
}
|
||
```
|
||
|
||
然后通过汇编来窥探下,在 `circle.diameter = 22` 处加断点,可以看到本质上调用的就是 `setter` 方法,`setter` 内部的实现就不一一窥探了
|
||
|
||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftStorePropertySetterDemo1.png" style="zoom:25%">
|
||
|
||
然后断点继续,在 `let diameter = circle.diameter` 处加断点,可以看到调用了 getter 方法
|
||
|
||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftStorePropertySetterDemo2.png" style="zoom:25%">
|
||
|
||
## 异同点
|
||
|
||
存储属性:
|
||
|
||
- 类似于成员变量
|
||
|
||
- 存储在实例的内存中
|
||
|
||
- 结构体、类可以定义存储属性
|
||
|
||
- 枚举不可以定义存储属性
|
||
|
||
- 在创建类、结构体的实例时,必须为所有的存储属性设置一个合适的初始值
|
||
|
||
- 延迟存储属性必须是 `var`,不能是 `let`。 因为 Swift 规定 let 必须在实例的初始化方法完成之前就拥有值
|
||
|
||
- `lazy` 在多线程情况下,无法保证属性只被初始化1次。
|
||
|
||
```swift
|
||
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 结构体和类的内存布局](./1.113.md) 探究过 `struct` 的内存布局,`struct` 的成员变量在内存中是连续存储的。由于是延迟存储属性,等真正使用的时候才会执行延迟属性的初始化逻辑,才会更改 `struct `的内存,所以 `let` 无法满足更改内存的需求。
|
||
|
||
```swift
|
||
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 的本质:只读的计算属性,不占用实例内存。
|
||
|
||
```swift
|
||
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)
|
||
```
|
||
|
||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftEnumRawValueExplore.png" style="zoom:25%">
|
||
|
||
通过汇编可以可以看到,在调用 `enum` 的 `rawvalue` 的时候本质是通过计算属性调用 `getter` 来实现的。
|
||
|
||
|
||
|
||
|
||
|
||
## 属性观察器
|
||
|
||
- 可以为非 `lazy` 的 `var` 存储属性设置属性观察期
|
||
|
||
- 在初始化器中设置属性值不会触发 `willSet`、`didSet`
|
||
|
||
- 属性观察器、计算属性的功能,同样可以应用在全局变量、局部变量上
|
||
|
||
```swift
|
||
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 核心原理
|
||
|
||
### 普通的存储属性
|
||
|
||
```swift
|
||
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`。
|
||
|
||
|
||
|
||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftInoutExploreDemo1.png" style="zoom:25%">
|
||
|
||
然后看到17行的关键代码,LLDB 输入 `si`,可以看到在第6行 `movq $0x14, (%rdi)`,将16进制的 `0x14` 也就是20,移动到指定的内存地址 `rdi` 上
|
||
|
||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftInoutExploreDemo2.png" style="zoom:25%">
|
||
|
||
因为 `struct` 结构体内存布局中,成员变量在内存中是连续存储的。所以 `struct `的地址也就是 `struct` 中第一个成员变量 `width` 的地址。
|
||
|
||
|
||
|
||
总结:普通的存储属性,在调用方法的时候,如果参数是 `inout` 传递引用,则直接传递存储属性的地址值即可。在方法内部,对该地址对应的值进行修改即可。
|
||
|
||
|
||
|
||
### 计算属性
|
||
|
||
对调用的代码进行调整
|
||
|
||
```swift
|
||
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)` 处下断点,查看汇编
|
||
|
||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftInoutExploreDemo3.png" style="zoom:25%">
|
||
|
||
核心思路:方法参数用 `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`
|
||
|
||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftInoutExploreDemo4.png" style="zoom:25%">
|
||
|
||
|
||
|
||
- 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` 方法。
|
||
|
||
这个流程下来,满足了修改值,且触发了原始属性观察器的需求。
|
||
|
||
|
||
|
||
### 带有属性观察器的存储属性
|
||
|
||
对调用的代码进行调整
|
||
|
||
```swift
|
||
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)` 处添加断点,查看汇编
|
||
|
||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftInoutExploreDemo5.png" style="zoom:25%">
|
||
|
||
分析:
|
||
|
||
- 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`
|
||
|
||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftInoutExploreDemo6.png" style="zoom:25%">
|
||
|
||
|
||
|
||
总结:带有属性观察器的存储属性,如果调用的方法参数是 `inout` 类型,系统真正实现,不能直接将对应的地址传进去,因为传进去很多简单,满足了修改值的需求。但是属性观察器的 `willSet`、`didSet` 就没办法触发了。所以为了触发属性观察器系统的设计是:
|
||
|
||
- 第一步:先将传递进去的地址,保存在函数的栈地址空间内的某个内存上
|
||
- 第二步:然后调用带有 `inout` 属性的方法,将步骤一得到的内存传递当作参数传进去。修改该内存对应的值
|
||
- 第三步:将步骤二得到的值后,调用一个 `setter` 方法,`setter` 方法内,先调用 `willSet`,再修改真正的带有观察属性的存储属性的值,最后调用 `didSet` 方法。
|
||
|
||
这个流程下来,满足了修改值,且触发了原始属性观察器的需求。
|
||
|
||
|
||
|
||
|
||
|
||
### 总结
|
||
|
||
1. 如果实参有内存地址,且没有设置属性观察器和计算属性,实现是直接将实参的内存地址传入带 `inout` 函数
|
||
|
||
2. 如果实参是计算属性或者设置了属性观察器:系统采用了 Copy In Copy Out 的策略。
|
||
1. 调用带 `inout` 函数时,先复制实参的值,产生副本 (getter,栈空间上的局部变量)
|
||
2. 将副本的内存地址传入带 `inout` 函数(副本进行引用传递),在函数内部修改副本的值
|
||
3. 函数返回后,再将副本的值覆盖实参的值(setter,willSet、set)
|
||
|
||
|
||
|
||
## 类型属性
|
||
|
||
- 不同于存储属性,类型属性必须设置初始值,永伟类型属性不像存储属性那样有 `init` 初始化器来初始化存储属性
|
||
|
||
- 类型属性就不存储在每个实例的内存里
|
||
|
||
- 存储属性默认就是 `lazy`,会在第一次使用的时候才初始化
|
||
|
||
- 存储属性就算被多个线程同时访问,但系统会保证只初始化1次
|
||
|
||
- 存储类型属性可以是 `let`
|
||
|
||
- 存储属性可以用 `class`、`static` 修饰
|
||
|
||
- 枚举 enum 里面不可以实例存储属性,但是可以定义类型存储属性
|
||
|
||
```swift
|
||
enum Season {
|
||
static let age: Int = 0
|
||
case spring, summer, antumn, winter
|
||
}
|
||
var season = Season.summer
|
||
```
|
||
|
||
- 类型属性的经典场景就是单例模式
|
||
|
||
```swift
|
||
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:
|
||
|
||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftTypePropertyDemo1.png" style="zoom:25%">
|
||
|
||
`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
|
||
|
||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftTypePropertyDemo2.png" style="zoom:25%">
|
||
|
||
`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次。
|