mirror of
https://github.com/NohamR/knowledge-kit.git
synced 2026-05-24 20:00:37 +00:00
update: 动态库、静态库的编译链接细节
This commit is contained in:
@@ -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. 函数返回后,再将副本的值覆盖实参的值(setter,willSet、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次,线程安全。
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user