Files
knowledge-kit/Chapter1 - iOS/1.115.md
2026-01-02 10:28:57 +08:00

674 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 属性
## 实例相关属性分类
### 存储属性
英文叫 Stored Property
- 类似于成员变量的概念
- 为什么叫存储属性?属性的内存直接存储在实例的内存中
- 结构体、类,都有存储属性
- **枚举不可以定义存储属性**
### 为什么 enum 不可以定义存储属性?
最基础的枚举内存占用1个字节只用来存储哪个 case 的索引值
```swift
enum Season {
case spring
case summer
case antumn
case winter
}
```
带有关联值的枚举
```swift
enum Season {
case spring(Int, Int, Int)
case summer(Int, Int)
case antumn(Int)
case winter(Bool)
case unknown
}
var season: Season = Season.spring(1, 2, 3)
print(MemoryLayout<Season>.size) // 3*8 + 1
print(MemoryLayout.stride(ofValue: season)) // 32
```
`.spring` 有3个 Int单个 Int 占8个字节空间所以红色框代表 spring 的1蓝色框代表 spring 的2绿色框代表 spring 的3黄色框代表枚举的第1个 case剩余7个字节为空。后续的7个字节是为了内存对齐而补齐的内存。
1个字节用来表达位置信息。共 `3*8 + 1 = 25`
内存对齐系数以8 Byte 为单位对象分配的内存必须是该值的整数倍所以实际分配后的内存为32字节
带有原始值的枚举
```swift
enum Season : Int {
case spring = 1
case summer = 2
case antumn = 3
case winter = 4
}
print(MemoryLayout<Season>.size) // 1
print(MemoryLayout<Season>.stride) // 1
print(MemoryLayout<Season>.alignment) // 1
```
只带有原始值的枚举同样只占用1个字节该字节的值为枚举的位置索引比如1、2而非原始值。原始值不占用枚举的内存
总结:
- Swift 的枚举是一种**值类型**,核心目的是表示一组**互斥的、有限的可能性**
- 允许存储属性,当枚举实例处于不同 case 时,这些属性的存在性和内存占用会变得不可预测
### 计算属性
英文名为Computed Property
- 计算属性的本质是方法
- 不占用实例的内存
- 计算属性只能用 var不能用 let
- 有了 setter必须有 getter
- 可以只有 getter没有 setter
- **枚举、结构体、 类都可以定义计算属性**
```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
print(MemoryLayout<Circle>.size) // 8
print(MemoryLayout<Circle>.stride) // 8
print(MemoryLayout<Circle>.alignment) // 8
```
计算属性 `y` 等价于下面的代码:
```swift
setDiameter (newValue: Int) {
radius = newValue/2
}
getDiameter () {
return 2*radius
}
```
然后通过汇编来窥探下,在 `circle.diameter = 22` 处加断点,可以看到本质上调用的就是 `setter` 方法,`setter` 内部的实现就不一一窥探了
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SwiftStorePropertySetterDemo1.png" style="zoom:25%">
然后断点继续,在 `let diameter = circle.diameter` 处加断点,可以看到调用了 getter 方法
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SwiftStorePropertySetterDemo2.png" style="zoom:25%">
计算属性的本质就是方法看上去是属性但是不占用结构体的内存。而是独立在代码段中所以只占用1个 Int 即8个字节的大小。
- **计算属性可以有只读计算属性**
也就是只有 getter没有 setter 方法
```swift
struct Circle {
var radius: Int // 存储属性
var diameter: Int { radius*2 }
}
```
### 延迟存储属性
常规写法
```swift
class Car {
init () {
print("Car init")
}
func run () {
print("Car is running")
}
}
class Person {
let car:Car = Car()
init () {
print("Person init")
}
func goOut() {
car.run()
}
}
let p = Person()
print("---")
p.goOut()
// console
Car init
Person init
---
Car is running
```
但是想实现一个需求,就是在 Person 初始化的时候先不初始化 Car当调用 Person 对象的 goOut 方法的时候再初始化,该怎么办?
**使用 lazy 可以定义一个延迟存储属性,在第一次用到属性的时候才会进行初始化**
对 Person 改造如下
```swift
class Person {
lazy var car:Car = Car()
init () {
print("Person init")
}
func goOut() {
car.run()
}
}
// console
Person init
---
Car init
Car is running
```
注意:延迟属性 lazy 必须和 var 搭配使用,不能是 let
## 异同点
存储属性:
- 类似于成员变量
- 存储在实例的内存中
- 结构体、类可以定义存储属性
- 枚举不可以定义存储属性
- 在创建类、结构体的实例时,必须为所有的存储属性设置一个合适的初始值
- 延迟存储属性必须是 `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://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SwiftEnumRawValueExplore.png" style="zoom:25%">
通过汇编 `SwiftDemo.Season.rawValue.getter` 可以看到,在调用 **`enum` 的 `rawvalue` 的时候本质是通过计算属性调用 `getter` 来实现的**。
类似于:
```swift
enum Season: Int {
case spring = 10
case summer = 20
case autumn = 30
case winter = 40
var rawValue: Int {
get {
switch self {
case .spring:
return 10
case .summer:
return 20
case .autumn:
return 30
case .winter:
return 40
}
}
}
}
let season = Season.summer
print(season.rawValue)
```
也侧面证明了 rawValue 不占用枚举的内存空间(是方法,存储在代码段)
## 属性观察器
- 可以为非 `lazy` 的 `var` 存储属性设置属性观察期
- 计算属性由于有 set 和 get因此不能有属性观察器 willSet 和 didSet
- 在初始化器中设置属性值不会触发 `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://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SwiftInoutExploreDemo1.png" style="zoom:25%">
然后看到17行的关键代码LLDB 输入 `si`可以看到在第6行 `movq $0x14, (%rdi)`将16进制的 `0x14` 也就是20移动到指定的内存地址 `rdi` 上
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/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://raw.githubusercontent.com/FantasticLBP/knowledge-kit/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://raw.githubusercontent.com/FantasticLBP/knowledge-kit/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://raw.githubusercontent.com/FantasticLBP/knowledge-kit/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://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SwiftInoutExploreDemo6.png" style="zoom:25%">
总结:带有属性观察器的存储属性,如果调用的方法参数是 `inout` 类型,系统真正的实现,并不是直接将 inout 参数的地址传进去,因为传进去很简单,满足了修改值的需求,但属性观察器的 `willSet`、`didSet` 就没办法触发了。
问题症结是:**直接传递 inout 参数的地址,可以满足直接修改值的需求,但直接修改没办法触发属性观察器的 willSet 和 didSet**
所以为了触发属性观察器系统的设计是:
- 第一步:先将传递进去的地址,保存在函数的栈地址空间内的某个内存上
- 第二步:然后调用带有 `inout` 属性的方法,将步骤一得到的内存传递当作参数传进去。修改该内存对应的值
- 第三步:将步骤二得到的值后,调用一个 `setter` 方法,`setter` 方法内,先调用 `willSet`,再修改真正的带有观察属性的存储属性的值,最后调用 `didSet` 方法。
这个流程下来,满足了修改值,且触发了原始属性观察器的需求。
### 总结
1. 如果实参有内存地址,且没有设置属性观察器和计算属性,实现是直接将实参的内存地址传入带 `inout` 函数
2. 如果实参是计算属性或者设置了属性观察器:系统采用了 **Copy In Copy Out** 的策略
- 调用带 `inout` 函数时,先复制实参的值,产生副本 get。栈空间上的局部变量 rbx + offset
- 将副本的内存地址传入带 `inout` 函数(副本进行引用传递),在函数内部修改副本的值
- 函数返回后再将副本的值覆盖实参的值set。willSet、didSet
## 类型属性
- 不同于存储属性,类型属性必须设置初始值,永伟类型属性不像存储属性那样有 `init` 初始化器来初始化存储属性
- 类型属性就不存储在每个实例的内存里
- 存储属性默认就是 `lazy`,会在第一次使用的时候才初始化
- 存储属性就算被多个线程同时访问但系统会保证只初始化1次
- 存储类型属性可以是 `let`
- 存储属性可以用 `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://raw.githubusercontent.com/FantasticLBP/knowledge-kit/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://raw.githubusercontent.com/FantasticLBP/knowledge-kit/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` 也是内存连续的。所以**类型属性就是带有访问控制(必须通过类来访问)的全局变量**
### 类型属性是线程安全的
看个 Demo
```swift
class Manager {
static var count = Int.random(in: 1...100)
}
Manager.count = 10
Manager.count = 11
```
下断点可以看到,调用了**地址访问器函数**。为什么需要地址访问器函数:
- 线程安全初始化首次访问时触发初始化(可能包含 `dispatch_once` 逻辑)
- 抽象内存访问隐藏实际存储位置(可能位于不同段或延迟分配)
- 支持属性观察didSet通过封装访问点插入回调逻辑
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SwiftTypeProperytyDispatchOnce1.png" style="zoom:30%" />
lldb 输入 si 查看具体实现
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SwiftTypeProperytyDispatchOnce2.png" style="zoom:30%" />
可以看到底层调用了 `swift_once` 函数函数传递了2个参数 rsi 存储 dispatch_once 的 block 参数rdi 存储了 onceToken
继续敲 si 可以看到底层调用的就是 GCD 的 `dispatch_once` 函数。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SwiftTypeProperytyDispatchOnce3.png" style="zoom:30%" />
类型属性如何保证线程安全的?如何保证只会初始化一次
底层会调用 `swift_once` 进而调用 `dispatch_once_f``dispatch_once_t` 会传递一个函数地址进去执行,类型属性的初始化代码将会被包装成一个函数。 由 `dispatch_once_t` 保证线程安全和只初始化1次。
所以类型存储属性底层通过 dispatch_once 保证了只会初始化1次线程安全。