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

289 lines
12 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.
# Swift 结构体和类的内存布局
## 结构体初始化器
实验1在 struct 内部自己实现 init
```swift
struct Point {
var x: Int
var y: Int
init () {
x = 0
y = 0
}
}
var point = Point()
```
`init` 方法内第一行处加 断点,如下所示
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SwiftStructMemoryLayoutDemo1.png" style="zoom:25%">
实验2struct 内不自己加 init
```swift
struct Point {
var x: Int = 0
var y: Int = 0
}
var point = Point()
```
`var point = Point()`处加 断点,如下所示
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/StructMemoryLayoutDemo2.png" style="zoom:25%">
现象:可以看到加不加自定义初始化器的汇编代码基本相同。
结论:**如果没有为结构体声明初始化器编译器会自动生成1个初始化器。目的是保证所有成员都有初始值。**
## 结构体内存布局
```swift
struct CustomDate {
var year: Int
var month: Int
var isLeapYear: Bool
}
var date = CustomDate(year: 2024, month: 3, isLeapYear: false)
print(MemoryLayout<CustomDate>.size) // 17
print(MemoryLayout<CustomDate>.stride) // 24
print(MemoryLayout<CustomDate>.alignment) // 8
print(Mems.memStr(ofVal: &date)) // 0x00000000000007e8 0x0000000000000003 0x0000000000000000
```
Int 占8 ByteBool 占1 Byte共 2*8 + 1 = 17 Byte由于存在内存对齐所以17向上到24 Byte。
- 值语义:`struct` 是值类型,这意味着当你将一个 `struct` 赋值给另一个变量或传递给函数时,会创建一个新的副本。每个副本都有其自己的内存空间,对其中一个副本的修改不会影响其他副本。
- 内存连续性:`struct` 的成员变量在内存中是连续存储的,没有额外的内存开销(如对象指针或元数据)。这使得访问 `struct` 的成员变量非常高效。
- 内存对齐:为了确保访问效率,编译器可能会对 `struct` 的成员变量进行内存对齐。这意味着某些成员变量之间可能会有未使用的内存空间(填充字节)。这种对齐通常是基于目标平台的硬件架构和访问性能考虑。
- 嵌套结构体:如果 `struct` 包含其他 `struct` 或枚举作为成员,那么这些嵌套的类型也会按照它们自己的内存布局规则进行排列。
- 可变大小结构体:在某些情况下,`struct` 的大小可能不是固定的。例如,如果 `struct` 包含可变长度的数组或字符串,那么它的实际大小将取决于这些成员的大小。然而,即使在这种情况下,`struct` 的内存布局仍然是紧凑的,并且遵循相同的访问规则。
- 与类的比较:与 `class`(类)不同,`struct` 不需要额外的内存来存储类型信息、引用计数或其他元数据。这使得 `struct`通常比 `class` 更轻量级,并且在某些情况下具有更好的性能。
## 类的内存布局
类和结构体类似,但是编译器不会为类自动生成可以传入成员值的初始化器。
```swift
// 1
class CustomDate {
var year: Int = 2024
var month: Int = 3
}
// 2
class CustomDate {
var year: Int
var month: Int
init () {
year = 2024
month = 3
}
}
```
上面2个写法是等价的。
结构体和类的区别:
- 结构体是值类型(枚举也是值类型),类是引用类型(指针类型)
值类型赋值给 var、let 或者给函数传参,是直接将所有内容拷贝一份。产生了全新副本,属于深拷贝。
### 值类型
```swift
func test() {
struct Point {
var x: Int
var y: Int
}
var point1 = Point(x: 10, y: 20)
var point2 = point1
point2.x = 11
point2.y = 22
print(point1.x) // 10
print(point1.x) // 20
print("over")
}
test()
```
因为是在函数内部变量,所以是在栈上分布
| 内存地址 | 内存数据 | 说明 |
| -------- | ---------------------------------- | -------- |
| 0x10000 | 10 ----赋值改变------> 11 | point2.x |
| 0x10008 | 20 ----赋值改变------> 22 | point2.y |
| 0x10010 | 10 | point1.x |
| 0x10018 | 20 | point1.y |
断点打在 `var point1 = Point(x: 10, y: 20)` 处,查看汇编
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SwiftValuePassDemo1.png" style="zoom:25%">
乍一看如果不认识的话,先找字面量(立即数),比如红色框内的 `0xa`,就是 10`0x14` 就是20。[之前](./109.md)学过寄存器的设计64位寄存器是兼容32位寄存器的。红色框内将 `0xa`,也就是 10 保存到 `%edi ` 寄存器内部,也就是保存到 `%rdi` 中,将 `0x14` 也就是20保存到 `%esi` 也就是保存到 `%rsi` 寄存器中。
LLDB 模式下输入 `si` 进入 init 方法内部。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SwiftValuePassDemo2.png" style="zoom:25%">
可以查看到将 `%rsi` 里的10 保存到 `%rdx` 中了;将 `%rdi` 里的20 保存到 `%rax` 中了。这也就是 struct `init` 方法做的事情。
LLDB 模式下输入 `finish` 结束 init 方法。
可以看到下面几句汇编
```assembly
0x1000035ac <+44>: movq %rax, -0x10(%rbp) // rbp - 0x10
0x1000035b0 <+48>: movq %rdx, -0x8(%rbp) // rbp - 0x8
0x1000035b4 <+52>: movq %rax, -0x20(%rbp) // rbp - 0x20
0x1000035b8 <+56>: movq %rdx, -0x18(%rbp) // rbp - 0x18
0x1000035bc <+60>: movq $0xb, -0x20(%rbp)
0x1000035c4 <+68>: movq $0x16, -0x18(%rbp)
```
可以看到分别将 `%rax` 里的10赋值给内存地址为 `%rbp - 0x10` `%rdx` 里的20赋值给内存地址为 `%rbp - 0x8` 了。
可与看到 `0x10``0x8` 地址相差8且地址连续也就是 point1 的内存地址。同样下面的 `0x20``0x18` 地址相差8且地址连续也就是 point2 的内存地址。
第五行将 `0xb` 也就是11 赋值给 `%rbp - 0x20`的地址,`0x16` 也就是22赋值给 `%rbp-0x18`的地址,也就是 point2 的 x、y
### COW 机制
**值类型的赋值操作Swift 标准库中的 String、Array、Dictionary 和 Set 确实采用了 Copy-On-WriteCOW写时复制 技术,这是一种内存优化策略,旨在提升性能并减少不必要的内存复制**
核心思想:
- **延迟复制**:当多个变量引用同一份数据时,它们共享底层存储,直到某个变量尝试修改数据时,才会真正复制一份独立的副本。
- **节省资源**:避免对不可变数据进行冗余复制,减少内存占用和计算开销
仅当有“写”操作时,才会真正执行拷贝操作:
- 对于标准库值类型的赋值操作Swift 能确保最佳性能,所以没必要为了保证最佳性能来避免赋值
- 自定义的值类型,比如结构体,在赋值的时候,就会立马发生深拷贝
举个例子
```swift
var array1 = [1, 2, 3]
var array2 = array1 //
array2.append(4) // COWarray1 array2
```
工作过程:
- 赋值时:`array2``array1` 共享同一块内存
- 修改时:当 `array2` 被修改时,检查引用计数。如果引用计数 > 1即存在多个所有者则复制底层存储确保修改不影响其他变量
写操作触发检查机制:
- **修改前检查**:执行写操作(删除、添加、修改)时,检查缓冲区的引用计数
- **唯一性检查**若引用计数为1则直接修改缓冲区否则复制缓冲区并修改新副本
伪代码
```swift
// 伪代码逻辑
mutating func append(_ element: Element) {
if !isUniquelyReferenced(&buffer) {
buffer = buffer.copy() // 复制缓冲区
}
buffer.append(element) // 修改新副本
}
```
#### 什么是缓冲区
Array 结构体(值类型)
+-------------------+
| 指向缓冲区的指针 |-----→ Buffer 类(引用类型)
| | +----------------+
| 其他元数据(长度、容量) | | 存储元素的内存块 |
+-------------------+ | [1, 2, 3, ...] |
+----------------+
1. **结构体轻量级**
`Array` 结构体本身只包含一个指针和少量元数据(如长度、容量),占用固定大小(如 8 字节指针 + 8 字节长度 + 8 字节容量 = 24 字节)。
2. **缓冲区动态分配**
实际存储元素的连续内存块由缓冲区动态分配在堆上,容量可扩展。
3. **共享与复制**
- **赋值时**:仅复制结构体的指针(浅拷贝),多个数组共享同一缓冲区。
- **修改时**:通过 COW写时复制机制仅在需要时复制缓冲区。
### 引用类型
引用赋值给 var、let 或者给函数传参,是将内存地址拷贝一份。属于浅拷贝
```swift
func testReferenceType() {
class Size {
var width: Int
var height: Int
init(width: Int, height: Int) {
self.width = width
self.height = height
}
}
var size1 = Size(width: 10, height: 20)
var size2 = size1
size2.width = 11
size2.height = 22
}
testReferenceType()
```
下断点,可以看到下面的汇编:
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ClassReferenceTypeMemoryLayoutDemo1.png" style="zoom:25%">
在调用(汇编的 call完 `allocating_init` 方法后,方法返回值用 `%rax` 保存的。然后打印出 `%rax` 寄存器的值,查看内存信息如下
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ClassReferenceTypeMemoryLayoutDemo2.png" style="zoom:25%">
红色框代表类信息的地址蓝色框代表引用计数绿色框代表10黄色框代表20.
汇编的第17行`movq %rdi, -0x50(%rbp)` 应该就是将 `%rdi` 里面的 对象内存地址赋值到 `%rbp - 0x50` 中去,也就是 `size1` 指针地址。
汇编的第20行`movq %rdi, -0x10(%rbp)` 应该就是将 `%rdi` 里面的 对象内存地址赋值到 `%rbp - 0x10` 中去,也就是 `size12 指针地址。
再接下去的汇编
```swift
0x100003525 <+133>: movq -0x50(%rbp), %rax
0x100003529 <+137>: movq $0xb, 0x10(%rax)
0x100003531 <+145>: callq 0x100007434 ; symbol stub for: swift_endAccess
0x100003536 <+150>: movq -0x50(%rbp), %rdi
0x10000353a <+154>: callq 0x100007476 ; symbol stub for: swift_release
0x10000353f <+159>: movq -0x68(%rbp), %rdx
0x100003543 <+163>: movq -0x60(%rbp), %rcx
0x100003547 <+167>: movq -0x50(%rbp), %rdi
0x10000354b <+171>: addq $0x18, %rdi
0x10000354f <+175>: leaq -0x48(%rbp), %rsi
0x100003553 <+179>: movq %rsi, -0x58(%rbp)
0x100003557 <+183>: callq 0x100007410 ; symbol stub for: swift_beginAccess
0x10000355c <+188>: movq -0x58(%rbp), %rdi
0x100003560 <+192>: movq -0x50(%rbp), %rax
0x100003564 <+196>: movq $0x16, 0x18(%rax)
```
可以看到将 `%rbp - 0x50 ` 的值赋值给 `%rax` ,然后将 `oxb` 也就是 11 保存到 `%rax` 也就是 size1 指针的所指向的内存的 `0x10`为什么是前面空了16位因为前8位保存类信息、后8位保存引用计数信息所以从16位开始。
`movq $0x16, 0x18(%rax)``0x16` 也就是 22 保存到 `%rax` 也就是 size1 指针的所指向的内存的 `0x18`为什么是前面空了24位因为前8位保存类信息、中间8位保存引用计数信息后8位保存 Int 的 width所以从24位开始。