Files
knowledge-kit/Chapter1 - iOS/1.113.md
2024-04-27 13:01:58 +08:00

226 lines
9.0 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://github.com/FantasticLBP/knowledge-kit/raw/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://github.com/FantasticLBP/knowledge-kit/raw/master/assets/StructMemoryLayoutDemo2.png" style="zoom:25%">
结论:结构体会有一个编译器自动生成的初始化编译器。目的是保证所有成员都有初始值。
实验3
```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://github.com/FantasticLBP/knowledge-kit/raw/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://github.com/FantasticLBP/knowledge-kit/raw/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
**Swift 标准库中为了提升性能String、Array、Dictionary、Set 采取了 Copy On Write 技术**
比如仅当有“写”操作时,才会真正执行拷贝操作
对于标准库值类型的赋值操作Swift 能确保最佳性能,所以没必要为了保证最佳性能来避免赋值。
### 引用类型
引用赋值给 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://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ClassReferenceTypeMemoryLayoutDemo1.png" style="zoom:25%">
在调用(汇编的 call`allocating_init` 方法后,方法返回值用 `%rax` 保存的。然后打印出 `%rax` 寄存器的值,查看内存信息如下
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/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位开始。