update: 动态库、静态库的编译链接细节

This commit is contained in:
FantasticLBP
2025-06-23 01:18:55 +08:00
parent aca020701b
commit 1142064d28
129 changed files with 10932 additions and 2615 deletions

View File

@@ -1,13 +1,95 @@
# 属性
## 实例相关属性分类
### 存储属性
英文叫 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 {
var radius: Int //
var diameter: Int { //
set {
radius = newValue/2
}
@@ -21,6 +103,10 @@ var circle = Circle(radius: 10)
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` 等价于下面的代码:
@@ -36,11 +122,94 @@ getDiameter () {
然后通过汇编来窥探下,在 `circle.diameter = 22` 处加断点,可以看到本质上调用的就是 `setter` 方法,`setter` 内部的实现就不一一窥探了
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftStorePropertySetterDemo1.png" style="zoom:25%">
<img src="./../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%">
<img src="./../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
## 异同点
@@ -131,11 +300,40 @@ let season = Season.summer
print(season.rawValue)
```
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftEnumRawValueExplore.png" style="zoom:25%">
<img src="./../assets/SwiftEnumRawValueExplore.png" style="zoom:25%">
通过汇编可以可以看到,在调用 `enum` 的 `rawvalue` 的时候本质是通过计算属性调用 `getter` 来实现的。
通过汇编 `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 不占用枚举的内存空间(是方法,存储在代码段)
@@ -143,6 +341,8 @@ print(season.rawValue)
- 可以为非 `lazy` 的 `var` 存储属性设置属性观察期
- 计算属性由于有 set 和 get因此不能有属性观察器 willSet 和 didSet
- 在初始化器中设置属性值不会触发 `willSet`、`didSet`
- 属性观察器、计算属性的功能,同样可以应用在全局变量、局部变量上
@@ -231,11 +431,11 @@ width is 20, side is 4, girth is 80
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftInoutExploreDemo1.png" style="zoom:25%">
<img src="./../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%">
<img src="./../assets/SwiftInoutExploreDemo2.png" style="zoom:25%">
因为 `struct` 结构体内存布局中,成员变量在内存中是连续存储的。所以 `struct `的地址也就是 `struct` 中第一个成员变量 `width` 的地址。
@@ -262,13 +462,13 @@ 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%">
<img src="./../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` 之间都是函数的栈空间
- 20行将 `movq %rax, -0x28(%rbp)` 函数返回值 `rax` 存放在 `main` 函数的栈空间上(虽然没有写调用函数,但是代码主入口就是 `main` 函数),且 `rbp` 到 `rsp` 之间都是函数的栈空间。也就是一个局部变量
- 21行 `leaq -0x28(%rbp), %rdi` 将栈空间上 `-0x28(%rbp)` 的地址值赋值给 `rdi` 寄存器
@@ -276,7 +476,7 @@ width is 5, side is 4, girth is 20
- 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%">
<img src="./../assets/SwiftInoutExploreDemo4.png" style="zoom:25%">
@@ -311,7 +511,7 @@ 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%">
<img src="./../assets/SwiftInoutExploreDemo5.png" style="zoom:25%">
分析:
@@ -329,11 +529,15 @@ width is 10, side is 20, girth is 200
- LLDB 输入 `si`,可以看到 `setter` 方法内部,调用了 `willSet`、`didSet`
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftInoutExploreDemo6.png" style="zoom:25%">
<img src="./../assets/SwiftInoutExploreDemo6.png" style="zoom:25%">
总结:带有属性观察器的存储属性,如果调用的方法参数是 `inout` 类型,系统真正实现,不能直接将对应的地址传进去,因为传进去很简单,满足了修改值的需求。但是属性观察器的 `willSet`、`didSet` 就没办法触发了。所以为了触发属性观察器系统的设计是:
总结:带有属性观察器的存储属性,如果调用的方法参数是 `inout` 类型,系统真正实现,并不是直接将 inout 参数的地址传进去,因为传进去很简单,满足了修改值的需求,但属性观察器的 `willSet`、`didSet` 就没办法触发了。
问题症结是:**直接传递 inout 参数的地址,可以满足直接修改值的需求,但直接修改没办法触发属性观察器的 willSet 和 didSet**
所以为了触发属性观察器系统的设计是:
- 第一步:先将传递进去的地址,保存在函数的栈地址空间内的某个内存上
- 第二步:然后调用带有 `inout` 属性的方法,将步骤一得到的内存传递当作参数传进去。修改该内存对应的值
@@ -348,11 +552,10 @@ width is 10, side is 20, girth is 200
### 总结
1. 如果实参有内存地址,且没有设置属性观察器和计算属性,实现是直接将实参的内存地址传入带 `inout` 函数
2. 如果实参是计算属性或者设置了属性观察器:系统采用了 Copy In Copy Out 的策略。
1. 调用带 `inout` 函数时,先复制实参的值,产生副本 getter栈空间上的局部变量
2. 将副本的内存地址传入带 `inout` 函数(副本进行引用传递),在函数内部修改副本的值
3. 函数返回后再将副本的值覆盖实参的值setterwillSet、set
2. 如果实参是计算属性或者设置了属性观察器:系统采用了 **Copy In Copy Out** 的策略
- 调用带 `inout` 函数时,先复制实参的值,产生副本 get。栈空间上的局部变量 rbx + offset
- 将副本的内存地址传入带 `inout` 函数(副本进行引用传递),在函数内部修改副本的值
- 函数返回后再将副本的值覆盖实参的值set。willSet、didSet
@@ -368,9 +571,9 @@ width is 10, side is 20, girth is 200
- 存储类型属性可以是 `let`
- 存储属性可以用 `class`、`static` 修饰
- 存储属性可以用 `static` 修饰
- 枚举 enum 里面不可以实例存储属性,但是可以定义类型存储属性
- **枚举 enum 里面不可以实例存储属性,但是可以定义类型存储属性**
```swift
enum Season {
@@ -398,9 +601,11 @@ width is 10, side is 20, girth is 200
### 内存角度分析:类型属性存储在哪
Demo1:
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftTypePropertyDemo1.png" style="zoom:25%">
<img src="./../assets/SwiftTypePropertyDemo1.png" style="zoom:25%">
`movq $0xa, 0x86d1(%rip) ` num1 的地址为: `rip + 0x86d1 = 0x100003b0f + 0x86d1 = 0x10000C1E0 `
@@ -414,7 +619,7 @@ Demo1:
Demo2
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftTypePropertyDemo2.png" style="zoom:25%">
<img src="./../assets/SwiftTypePropertyDemo2.png" style="zoom:25%">
`movq $0xa, 0x8b65(%rip)` num1 的内存为 `rip + 0x8b65 = 0x1000037c3 + 0x8b65 = 0x10000C328 `
@@ -422,12 +627,47 @@ Demo2
`movq $0xc, 0x8b38(%rip) ` num1 的内存为 `rip + 0x8b38 = 0x100003800 + 0x8b38 = 0x10000C338 `
可以看到 `0x10000C328` `0x10000c330` `0x10000C338` 也是内存连续的。所以类型属性就是带有访问控制(必须通过类来访问)的全局变量
可以看到 `0x10000C328` `0x10000c330` `0x10000C338` 也是内存连续的。所以**类型属性就是带有访问控制(必须通过类来访问)的全局变量**
Demo3
### 类型属性是线程安全的
类型属性如何保证线程安全的?如何保证只会初始化一次。
看个 Demo
底层会调用 `swift_once` 进而调用 `dispatch_once_t``dispatch_once_t` 会传递一个函数地址进去执行,类型属性的初始化代码将会被包装成一个函数。 由 `dispatch_once_t` 保证线程安全和只初始化1次。
```swift
class Manager {
static var count = Int.random(in: 1...100)
}
Manager.count = 10
Manager.count = 11
```
下断点可以看到,调用了**地址访问器函数**。为什么需要地址访问器函数:
- 线程安全初始化首次访问时触发初始化(可能包含 `dispatch_once` 逻辑)
- 抽象内存访问隐藏实际存储位置(可能位于不同段或延迟分配)
- 支持属性观察didSet通过封装访问点插入回调逻辑
<img src="./../assets/SwiftTypeProperytyDispatchOnce1.png" style="zoom:30%" />
lldb 输入 si 查看具体实现
<img src="./../assets/SwiftTypeProperytyDispatchOnce2.png" style="zoom:30%" />
可以看到底层调用了 `swift_once` 函数函数传递了2个参数 rsi 存储 dispatch_once 的 block 参数rdi 存储了 onceToken
继续敲 si 可以看到底层调用的就是 GCD 的 `dispatch_once` 函数。
<img src="./../assets/SwiftTypeProperytyDispatchOnce3.png" style="zoom:30%" />
类型属性如何保证线程安全的?如何保证只会初始化一次
底层会调用 `swift_once` 进而调用 `dispatch_once_f``dispatch_once_t` 会传递一个函数地址进去执行,类型属性的初始化代码将会被包装成一个函数。 由 `dispatch_once_t` 保证线程安全和只初始化1次。
所以类型存储属性底层通过 dispatch_once 保证了只会初始化1次线程安全。