Compare commits

...

8 Commits

Author SHA1 Message Date
84fc93bcc4 feat: update sweetpad configuration
All checks were successful
Deploy Pages / Generate-Pages (push) Successful in 36s
2025-05-16 13:32:59 +08:00
2266091d11 fix: github link
All checks were successful
Deploy Pages / Generate-Pages (push) Successful in 1m18s
2025-05-13 22:43:15 +08:00
1055927991 feat: remove post
All checks were successful
Deploy Pages / Generate-Pages (push) Successful in 1m33s
2025-05-13 22:15:36 +08:00
b6d8b90872 feat: update
All checks were successful
Deploy Pages / Generate-Pages (push) Successful in 1m33s
2025-05-13 21:26:35 +08:00
514d06ceba feat: update
All checks were successful
Deploy Pages / Generate-Pages (push) Successful in 1m8s
2025-05-13 13:37:15 +08:00
11bc5921ab feat: update 2025-05-13 01:12:14 +08:00
bec3aaa73d
fix: update callback url & runnumber 2025-04-20 16:41:35 +08:00
59473df655
feat: add post
All checks were successful
Deploy Pages / Generate-Pages (push) Successful in 2m35s
2025-03-17 21:20:45 +08:00
6 changed files with 1456 additions and 4 deletions

View File

@ -26,7 +26,7 @@ jobs:
subject: 博客部署启动
to: 3145078758@qq.com
from: Gitea Actions
body: 博客自动化部署开始执行, 前往 ${{ gitea.server_url }}/Linloir/blog/actions/runs/${{ gitea.run_id }} 查看执行日志
body: 博客自动化部署开始执行, 前往 ${{ gitea.server_url }}/Linloir/blog/actions/runs/${{ gitea.run_number }} 查看执行日志
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
@ -60,7 +60,7 @@ jobs:
commit_options: "--no-verify"
token: "${{ secrets.TOKEN }}"
- name: Call API to Update Caddy
run: curl https://upd.linloir.cn/update/blog.linloir.gitea.linloir.cn
run: curl https://upd.linloir.cn/update/blog.linloir.cn
- name: Send mail On Success
if: success()
uses: https://github.com/dawidd6/action-send-mail@v3
@ -86,4 +86,4 @@ jobs:
subject: 博客部署失败
to: 3145078758@qq.com
from: Gitea Actions
body: 博客自动化部署执行失败, 前往 ${{ gitea.server_url }}/Linloir/blog/actions/runs/${{ gitea.run_id }} 查看执行日志
body: 博客自动化部署执行失败, 前往 ${{ gitea.server_url }}/Linloir/blog/actions/runs/${{ gitea.run_number }} 查看执行日志

View File

@ -0,0 +1,773 @@
---
title: Coroutine 相关疑惑大模型问答记录
date: 2025-03-17 21:09:21
tags:
- 后端
- 编程语言
categories:
- 技术
---
近日在看 python 的 asyncio 时突然想起了曾经对 C++ coroutine 以及 go 的 goroutine 的诸多未解疑惑。还记得当时有这些疑惑的时候,还没有 ChatGPT 或是 DeepSeek 这样方便的大模型解惑,反复查找各种资料最后因为时间原因还是没有深究,而如今 DeepSeek 已然如此发达,遂借此机会解答遗留在心中多年的疑惑,并以此贴记录。
## Round 1
**Q:**
AsyncIO 或者说Coroutine 是怎么实现,例如在 await 一个网络请求的时候,进程不会被内核挂起进入等待状态,而是可以继续正常被调度并且执行余下内容的?同时,当网络请求完成的时候,内核又是怎么让进程知道请求完成从而继续 await 后续未执行的内容的
**A:**
> 嗯用户问的是AsyncIO或者说协程是怎么实现的特别是在await一个网络请求的时候进程不会被内核挂起而是可以继续执行其他任务。然后当网络请求完成后内核又是如何通知进程继续执行的。我需要详细解释AsyncIO和协程的工作原理特别是事件循环、非阻塞I/O以及操作系统层面的机制比如epoll或者kqueue。
>
> 首先用户可能已经了解一些基本的异步编程概念但想深入理解底层机制。需要分步骤解释从协程、事件循环到非阻塞I/O和操作系统接口。可能需要提到用户态和内核态的区别以及如何通过事件驱动的方式处理I/O操作避免阻塞。
>
> 然后用户可能对内核如何通知事件完成感兴趣这里需要解释I/O多路复用机制比如select、poll、epoll特别是epoll在Linux中的工作方式。当有I/O事件发生时内核如何通知应用程序从而触发回调函数恢复协程执行。
>
> 另外需要区分进程、线程和协程的不同。协程是用户态的轻量级线程由事件循环调度而不是由操作系统内核调度。这样在遇到I/O操作时可以挂起当前协程执行其他任务提高并发性能。
>
> 可能还需要提到Future和Task对象在AsyncIO中如何管理异步操作的状态以及await关键字如何挂起协程让出控制权给事件循环直到Future完成。
>
> 最后总结整个流程从发起网络请求到事件循环注册到操作系统处理I/O完成后的回调恢复协程执行。确保解释清晰技术细节准确同时避免过于晦涩。
AsyncIO/Coroutine 的实现核心在于**用户态协作式调度**和**操作系统非阻塞I/O机制**的结合。以下是技术实现的关键点:
---
### 一、核心机制
1. **事件循环 (Event Loop)**
通过单线程轮询I/O事件使用`epoll`(Linux)/`kqueue`(BSD)/`IOCP`(Windows)等系统调用实现高效事件监听:
```python
# 伪代码示例
while True:
events = epoll_wait(epoll_fd, timeout)
for fd, event in events:
callback = fd_to_callback[fd]
callback()
```
2. **非阻塞I/O**
通过`fcntl(fd, F_SETFL, O_NONBLOCK)`设置socket为非阻塞模式当I/O未就绪时立即返回`EWOULDBLOCK`错误,避免进程被挂起。
3. **协程调度**
使用生成器Python 3.5+的`async/await`)实现协程切换:
```python
async def fetch():
await socket.read() # 让出控制权
```
---
### 二、内核交互细节
1. **I/O准备阶段**
```mermaid
graph LR
A[发起socket.read()] --> B[注册到epoll]
B --> C[挂起当前协程]
C --> D[事件循环继续运行其他协程]
```
2. **内核通知机制**
当数据到达时,网卡通过**中断**通知内核内核将socket标记为就绪epoll会检测到该事件并通知用户态。
3. **回调触发**
事件循环收到内核通知后,通过`callback()`恢复对应协程的上下文继续执行。
---
### 三、性能关键指标
1. **上下文切换成本**
| 类型 | 切换成本 |
|------------|---------------|
| 进程切换 | ~1-10μs |
| 线程切换 | ~0.5-2μs |
| 协程切换 | ~0.1-0.3μs |
2. **I/O吞吐量对比**
```math
\text{Throughput} = \frac{\text{Requests}}{\text{Context Switch Time} + \text{I/O Latency}}
```
---
### 四、操作系统支持
1. **Linux epoll**
使用红黑树管理文件描述符,时间复杂度为$O(1)$的事件通知。
2. **BSD kqueue**
支持更多事件类型过滤但API更复杂。
3. **Windows IOCP**
真正的异步I/O模型与Overlapped I/O配合使用。
---
### 五、编程模型对比
| 特性 | 多进程 | 多线程 | 协程 |
|---------------|--------|--------|----------|
| 内存占用 | 高 | 中 | 低 |
| 切换成本 | 高 | 中 | 极低 |
| 并发能力 | 低 | 中 | 高 |
| 数据共享 | 困难 | 需同步 | 天然共享 |
---
### 六、典型工作流程
1. 协程调用`await socket.read()`
2. 事件循环将socket注册到epoll
3. 当前协程挂起保存上下文PC/寄存器/栈)
4. 事件循环继续执行其他协程
5. 内核通知数据到达epoll返回就绪事件
6. 事件循环调用注册的回调函数
7. 恢复挂起协程的上下文,继续执行
这种设计使得单线程即可实现数万并发连接如Nginx的C10K解决方案同时避免了内核态/用户态频繁切换的开销。
## Round 2
**Q:**
协程是否需要保存当前栈帧
**A:**
> 好的用户问协程是否需要保存当前栈帧。我需要先回顾之前关于协程和AsyncIO的讨论确保回答的一致性。用户可能已经了解协程的基本原理现在想深入技术细节。
>
> 首先,协程的挂起和恢复确实需要保存栈帧。因为协程在用户态切换,没有内核的调度,所以必须手动保存和恢复上下文。栈帧保存了局部变量、函数调用链等信息,这对恢复执行至关重要。
>
> 接下来我需要解释栈帧保存的具体内容比如寄存器状态、局部变量、返回地址等。可能需要用例子说明比如Python的生成器如何保存状态。同时要区分不同语言的实现比如Go的goroutine使用分段栈而Python的协程可能依赖生成器。
>
> 用户可能还想知道为什么需要保存这些信息,以及不保存会有什么后果。比如,如果栈帧不保存,恢复时无法继续执行,导致数据错误或崩溃。此外,栈的动态增长如何处理,比如是否使用协程私有栈,避免栈溢出影响其他协程。
>
> 还要考虑性能方面,栈帧保存的开销如何,比如对比线程的上下文切换。可能提到协程切换的高效性,因为只在用户态操作,无需陷入内核。
>
> 另外用户可能对具体实现机制感兴趣比如setjmp/longjmp、ucontext库或者汇编手动切换上下文。不同语言和库的实现方式不同但核心都是保存和恢复栈帧。
>
> 最后,需要总结栈帧保存的必要性,强调这是协程能够正确挂起和恢复的关键,确保用户理解其重要性。
协程的实现**必须完整保存当前栈帧**,这是实现正确挂起/恢复的核心机制。栈帧保存的完整性和实现方式直接决定了协程的性能和可靠性,具体技术细节如下:
---
### 一、栈帧保存的必要性
| 组件 | 必须保存的原因 | 丢失后果 |
|-----------------|---------------------------------|----------------------------|
| 返回地址 | 确定恢复执行的位置 | 程序计数器错乱导致崩溃 |
| 局部变量 | 维护函数调用上下文 | 数据不一致或计算错误 |
| 寄存器状态 | 保证指令连续执行 | 寄存器污染导致未定义行为 |
| 调用链信息 | 维护协程嵌套调用关系 | 栈回溯失败,调试信息丢失 |
---
### 二、栈帧保存的实现方式
#### 1. 轻量级方案Python等解释型语言
```python
# 生成器协程的栈帧保存
def coro():
x = 42
yield # 此处保存栈帧:
# - 局部变量x
# - 指令指针(IP)
# - 当前栈指针(SP)
print(x)
gen = coro()
next(gen) # 触发栈帧保存
```
**特点**
- 通过代码对象(__code__)和帧对象(f_lasti)记录状态
- 每个yield消耗约200字节内存
#### 2. 高性能方案C/C++/Rust
```cpp
// 使用ucontext库手动切换
void coroutine_entry(void* arg) {
while (true) {
// 业务逻辑
swapcontext(&ctx_current, &ctx_scheduler);
}
}
// 上下文切换时自动保存:
// - 所有通用寄存器 (rax, rbx...)
// - 浮点寄存器 (xmm0-15)
// - 栈指针 (rsp)
// - 指令指针 (rip)
// - 标志寄存器 (rflags)
```
**性能指标**
- 单次切换约23ns (x86) / 35ns (ARM)
- 内存开销约2KB/协程
#### 3. 零拷贝方案Go goroutine
```go
// Go使用分段栈实现协程
func coro() {
var big [8192]byte // 触发栈扩容
// ...
}
// 运行时自动管理:
// - 栈扩容时复制到新内存区域
// - 使用连续栈时维护拷贝指针
```
**优势**
- 栈空间自动增长,无最大限制
- 栈切换仅需修改寄存器,无需内存拷贝
---
### 三、栈帧保存的底层原理
#### 1. 寄存器级保存
```asm
; x86-64上下文保存示例
swapcontext:
push rbp
mov rbp, rsp
push rbx
push r12
push r13
push r14
push r15
mov [rdi + UC_MCONTEXT_GREGS + REG_RSP], rsp
mov [rdi + UC_MCONTEXT_GREGS + REG_RIP], rcx
; 共保存23个寄存器和浮点状态
```
#### 2. 栈内存管理
```c
// 协程私有栈实现
struct coroutine {
void* stack; // 独立栈空间
size_t stack_size;
jmp_buf env; // 保存的寄存器集合
};
// 切换时重置栈指针
void switch_to(struct coroutine* from, struct coroutine* to) {
_switch(&from->env, &to->env);
// 汇编实现栈指针切换:
// mov rsp, [to->env + REG_RSP]
// ret
```
#### 3. 栈溢出防护
```rust
// Rust async-std的栈保护机制
async fn deep_recursion() {
let guard = PageGuard::new(); // 分配保护页
recursive_call().await;
}
// 内存布局:
// [ 可用栈空间 | RED ZONE (不可访问) | 保护页 ]
// 访问保护页触发SIGSEGV自动扩容栈
```
---
### 四、不同语言的实现对比
| 语言 | 栈帧保存方式 | 切换开销 | 最大协程数 (8GB内存) |
|------------|----------------------------|----------|----------------------|
| Python | 生成器帧对象 | 120ns | ~500,000 |
| Go | 分段栈/连续栈 | 35ns | ~10,000,000 |
| C++ | ucontext/手动汇编 | 23ns | ~5,000,000 |
| Java(Loom) | 虚拟线程栈镜像 | 45ns | ~2,000,000 |
| Rust | async/await状态机编译时展开 | 18ns | ~8,000,000 |
---
### 五、特殊场景处理
1. **动态栈扩展**
当协程调用深度递归时:
```c
void coro() {
char buf[1024];
await read(); // 栈空间不足时:
// 1. 分配新栈
// 2. 拷贝旧栈数据
// 3. 修改栈指针
}
```
2. **跨栈引用保护**
C++20协程要求
```cpp
task<void> unsafe_coro() {
int local = 42;
co_await something(); // 挂起点之后
print(local); // 可能引用已释放栈!
}
// 正确做法使用shared_ptr持有跨栈数据
```
3. **垃圾回收协同**
Go的逃逸分析确保协程栈安全
```go
func coro() {
x := 42
go func() {
println(x) // x逃逸到堆
}()
}
```
---
### 结论
协程必须完整保存当前栈帧这是实现正确执行流控制的基石。现代运行时通过寄存器快照、独立栈空间、智能内存管理等技术在保证安全性的同时将切换开销控制在纳秒级。不同语言根据自身特性选择最优实现策略如Go的分段栈、Rust的零成本抽象等最终目标都是在用户态实现接近线程的编程模型同时获得更高的并发性能。
## Round 3
**Q:**
我好奇的一点是:如果函数 `A` 中调用了 `b = B()`,而 `B` 中调用了 `await post_http_request()`,我理解在 `post_http_request` 中会在发送完 http 请求后对 `post_http_request` 函数的栈帧内所有内容进行保存,然后返回到 `B` 中,由于 `B` 中调用了 `await`,则会保存 B 的栈帧,最后返回到 `A` 中继续执行,对吗?
我的疑问在于,在你前面给出的例子中,只保存了寄存器相关的值而没有保存栈上存储的内容,如果在这种情况下,`A` 函数继续执行,可能会覆盖原先 `B``post_http_request` 栈上的内容,这是否会导致切换失败?还是说,栈帧保存也会保存栈上的数据?这样不会消耗额外的时间来拷贝内存吗?
**A:**
> 嗯用户的问题是关于协程在挂起时如何保存栈帧的。他举了一个例子函数A调用BB中调用了await post_http_request()。用户想知道当post_http_request发送完HTTP请求后是否会保存它的栈帧然后返回到B再保存B的栈帧最后回到A继续执行。用户担心如果只保存寄存器而没有保存栈上的数据A继续执行可能会覆盖B和post_http_request的栈内容导致切换失败。或者是否需要保存栈上的数据这样会不会有内存拷贝的开销。
>
> 首先,我需要回顾之前关于协程保存栈帧的讨论。之前提到协程必须保存当前栈帧,包括寄存器、局部变量等。用户可能误解了栈帧保存的方式,认为只保存寄存器而没有保存栈上的数据,但实际上,正确的实现需要保存整个栈帧的内容,或者使用独立的栈空间来避免被覆盖。
>
> 接下来需要解释协程的不同实现方式。比如在Python这样的生成器协程中每个协程可能共享同一个栈通过生成器对象保存局部变量和指令指针但可能不保存整个栈帧。而在其他语言如Go或Rust中每个协程可能有自己的独立栈切换时切换栈指针这样就不会被其他协程覆盖。
>
> 用户提到的例子中当post_http_request被await挂起时协程需要保存当前的执行状态包括B函数和post_http_request的栈数据。如果所有协程共享同一个栈那么当A继续执行时确实可能覆盖之前的栈内容导致数据损坏。因此正确的实现需要每个协程有自己的栈空间或者在挂起时将栈数据拷贝到堆中保存。
>
> 需要解释不同语言的实现策略。例如Go使用分段栈或连续栈每个goroutine有自己的栈切换时只需切换栈指针无需拷贝数据。而Python的协程可能使用生成器保存必要的状态而非整个栈适用于浅层调用但深层嵌套可能有问题。
>
> 用户担心内存拷贝的开销需要说明高性能实现如Go、Rust如何通过独立栈来避免拷贝只在必要时增长栈或分配新栈从而减少开销。同时解释栈切换的机制比如修改栈指针寄存器使得每个协程使用不同的内存区域互不干扰。
>
> 可能还需要提到,当协程挂起时,其栈帧的数据必须保留在内存中,直到恢复执行。如果使用独立栈,这些数据不会被其他协程覆盖,因此无需拷贝。而如果共享栈,则需要将栈数据保存到堆中,这会带来拷贝开销,但现代实现通常避免这种方式,选择为每个协程分配独立栈空间。
>
> 总结用户的疑问点:是否保存栈数据,如何避免覆盖,以及性能影响。需要明确回答,协程实现中栈帧的保存方式,不同策略的优缺点,以及如何解决用户提到的覆盖问题。
协程的实现中,**栈帧保存机制**是保证正确性的核心,但具体实现方式在不同语言/框架中有显著差异。针对你的场景,我们分层次解析:
---
### 一、场景还原与问题本质
```python
async def A():
b = await B() # <- 挂起点1
async def B():
await post_http_request() # <- 挂起点2
```
当执行到`post_http_request()`中的`await`时:
1. **调用链**`A → B → post_http_request`
2. **挂起顺序**:从`post_http_request`开始逐层向上传递`await`
3. **关键问题**:如何保存整个调用链的上下文,避免栈数据被覆盖?
---
### 二、两种核心实现策略
#### 1. **共享栈模式 (Stackful Coroutines)**
- **代表实现**Go goroutine、C++ Boost.Coroutine
- **核心机制**
- 每个协程拥有**独立栈空间**初始大小通常为2KB-8KB
- 切换时**直接切换栈指针寄存器**如x86的`rsp`
- **无需拷贝栈数据**,通过内存隔离避免覆盖
```c
// Go调度器伪代码
func schedule() {
// 保存当前协程的rsp
old_gobuf.rsp = get_rsp()
// 切换到目标协程的栈
set_rsp(new_gobuf.rsp)
ret
}
```
- **内存布局**
```
┌─────────────┐
│ 协程X的栈 │ ← rsp_x
├─────────────┤
│ 协程Y的栈 │ ← rsp_y
└─────────────┘
```
- **优势**零拷贝切换极快约35ns
- **代价**:每个协程需预分配独立栈空间
#### 2. **状态机模式 (Stackless Coroutines)**
- **代表实现**Python AsyncIO、Rust async/await
- **核心机制**
- 通过编译器将协程转换为**状态机**
- **所有局部变量提升到堆分配的结构体**
- 栈帧数据**显式保存到堆内存**
```rust
// Rust async编译后伪代码
struct B_Future {
state: u32,
local_vars: B_Locals, // 包含所有局部变量
}
struct A_Future {
b_future: B_Future,
local_b: Option<B_Result>,
}
```
- **内存布局**
```
┌─────────────┐
│ 主线程栈 │ ← 仅存状态机指针
├─────────────┤
│ 堆内存 │ ← 存储所有协程状态
└─────────────┘
```
- **优势**:内存占用低(无独立栈)
- **代价**局部变量访问需间接寻址约多1-3 CPU周期
---
### 三、关键问题解答
#### Q1为何不会发生栈数据覆盖
- **共享栈模式**:每个协程有独立栈空间,物理内存不重叠
- **状态机模式**:所有协程共享主线程栈,但局部变量已提升到堆,栈上仅存指针
#### Q2栈数据是否需要显式拷贝
- **共享栈模式**:无需拷贝,切换时直接跳转栈指针
- **状态机模式**:编译器自动将局部变量"移动"到堆结构体,相当于隐式拷贝
#### Q3性能影响对比
| 指标 | 共享栈模式 | 状态机模式 |
|---------------------|----------------|----------------|
| 单次切换延迟 | ~35ns | ~18ns |
| 内存开销/协程 | 2KB-8KB | 200B-2KB |
| 局部变量访问速度 | 直接栈访问 | 间接堆访问 |
| 最大协程数 (8GB) | ~1,000,000 | ~10,000,000 |
---
### 四、具体语言实现剖析
#### 1. **Python AsyncIO**
- **实现方式**:生成器+状态机
- **栈保存机制**
- 每个`async def`函数转换为生成器
- 局部变量保存在生成器对象的`gi_frame`
- **不保存完整栈**,依赖事件循环调度
```python
async def B():
x = 42 # 存储在生成器对象的x属性
await post_http_request()
print(x) # 从生成器对象读取x
# 编译后伪代码
class B_Generator:
def __init__(self):
self.x = None
self.state = 0
def __next__(self):
if self.state == 0:
self.x = 42
self.state = 1
return post_http_request()
elif self.state == 1:
print(self.x)
```
- **缺陷**:无法处理深层递归调用(栈深度受限)
#### 2. **Rust async/await**
- **实现方式**:状态机 + 零成本抽象
- **栈保存机制**
- 编译器将每个`async fn`转换为结构体
- 所有局部变量成为结构体字段
- 通过`Pin`保证内存位置固定
```rust
async fn B() {
let x = 42; // 存储在B_Future结构体
post_http_request().await;
println!("{}", x);
}
// 编译后伪代码
struct B_Future {
state: u32,
x: i32,
http_request: PostHttpRequestFuture,
}
impl Future for B_Future {
fn poll(...) {
match self.state {
0 => {
self.x = 42;
self.state = 1;
self.http_request.poll()
}
1 => {
println!("{}", self.x);
Poll::Ready(())
}
}
}
}
```
- **优势**:无堆分配(可通过`no_std`运行)
---
### 五、实际内存布局示例
#### 场景:`A → B → post_http_request`挂起
- **共享栈模式 (Go)**
```
┌─────────────┐
│ 协程A栈 │ ← 保存B的返回地址和局部变量
├─────────────┤
│ 协程B栈 │ ← 保存post_http_request的上下文
└─────────────┘
```
- 每个协程栈独立,切换时硬件自动隔离
- **状态机模式 (Rust)**
```
┌─────────────┐
│ 主栈 │ → 仅存储当前活跃Future指针
├─────────────┤
│ 堆内存 │ ← A_Future { B_Future { PostHttpRequest_Future } }
└─────────────┘
```
- 所有状态嵌套存储在堆上
---
### 六、总结
- **共享栈协程**通过独立栈空间物理隔离数据,**无需拷贝但预分配内存**
- **状态机协程**通过编译期代码变换将栈数据提升到堆,**牺牲局部变量访问速度换取高密度**
- 现代语言如Go/Rust通过精细设计使得协程切换开销可控制在**20-50ns**,完全满足高并发需求
## Round 4
**Q:**
所以你的意思是协程函数会有自己独立的栈空间是吗?这样的话,调用协程函数是否是和调用不同函数时不同,例如需要额外切换 rsp 到对应协程函数所在的栈上?
**A:**
> 好的,我现在需要回答用户关于协程函数是否拥有独立栈空间以及调用协程函数是否需要切换栈指针的问题。用户之前的对话已经涉及协程的实现机制,包括共享栈和状态机模式的区别,以及栈帧保存的必要性。用户现在想确认协程是否使用独立栈空间,以及调用时是否需要切换栈指针。
>
> 首先我需要回顾之前的讨论。用户之前的问题集中在协程如何保存栈帧避免数据被覆盖。我的回答提到了共享栈和状态机两种模式其中共享栈模式每个协程有独立栈空间而状态机模式则通过编译器将局部变量提升到堆中。用户现在的问题直接询问协程是否有独立栈空间以及是否需要切换rsp寄存器。
>
> 接下来我需要明确回答用户的问题是的某些协程实现如共享栈模式确实使用独立栈空间并在调用时切换rsp。而状态机模式如Rust的async/await则不依赖独立栈而是将状态保存在堆上因此不需要切换栈指针。
>
> 需要分点详细解释两种模式的区别特别是栈的使用和切换机制。同时要举例说明不同语言中的实现如Go的goroutine使用共享栈而Rust使用状态机模式。此外需要比较两者的优缺点如性能、内存占用和适用场景。
>
> 可能还需要涉及技术细节如栈指针切换的具体操作如何通过修改rsp寄存器实现协程切换以及状态机模式如何避免栈切换。同时要解答用户可能的疑问比如为什么有的实现选择共享栈有的选择状态机各自的权衡是什么。
>
> 需要确保回答准确避免错误。例如Go的协程确实使用分段栈或连续栈而Rust的async/await在编译时转换为状态机不依赖独立栈空间。同时要指出切换栈指针的性能影响以及状态机模式在内存使用上的优势。
>
> 最后,总结两种模式的适用场景,帮助用户理解不同协程实现的优缺点,以便根据具体需求选择合适的模型。
协程是否拥有独立栈空间取决于具体的实现模型,不同编程语言和框架采用了不同的策略。以下是两种主流实现方式的详细对比:
---
### **一、共享栈协程 (Stackful Coroutines)**
**代表实现**Go goroutine、Lua coroutine、C++ Boost.Coroutine
**核心特性**每个协程拥有独立的栈空间切换时需要修改栈指针寄存器如x86的`rsp`)。
#### **1. 栈空间管理**
- **独立栈分配**
```go
// Go运行时为每个goroutine预分配栈初始2KB
go func() { // 创建新协程
myFunc() // 在独立栈中运行
}()
```
- **栈增长机制**
- **分段栈**旧版Go栈不足时分配新段通过`morestack`检测
- **连续栈**现代Go栈不足时整体复制到更大的内存区域
#### **2. 栈指针切换**
- **切换原理**
```asm
; x86-64协程切换伪代码
swapcontext:
push rbp
mov rbp, rsp
; 保存当前寄存器到旧协程上下文
mov [old_ctx + RSP_OFFSET], rsp
; 加载新协程的栈指针
mov rsp, [new_ctx + RSP_OFFSET]
; 恢复新协程的寄存器
pop rbp
ret
```
- **性能指标**
- 切换耗时约35-50ns包括保存/恢复16个通用寄存器+浮点寄存器)
- 内存开销每个协程至少2KBGo) 到 8KB (C++)
#### **3. 调用链示例**
```go
func A() {
B() // 切换到B的栈
}
func B() {
C() // 切换到C的栈
}
```
**内存布局**
```
┌─────────────┐
│ 主线程栈 │ ← 0x7ffeefbff000
├─────────────┤
│ Goroutine A栈 │ ← 0xc000032000
├─────────────┤
│ Goroutine B栈 │ ← 0xc0000ac000
└─────────────┘
```
---
### **二、无栈协程 (Stackless Coroutines)**
**代表实现**Rust async/await、Python AsyncIO、JavaScript async
**核心特性**:协程共享主线程栈,通过状态机管理上下文,无需切换栈指针。
#### **1. 实现原理**
- **状态机转换**
```rust
async fn example() {
let a = 1; // 状态0
some_io().await; // 状态1
println!("{}", a);
}
```
编译后展开为:
```rust
struct ExampleFuture {
state: u32,
a: i32,
io_future: SomeIOFuture,
}
```
- **栈共享机制**
- 所有局部变量存储在堆上的Future对象中
- 主线程栈仅保留当前活跃Future的指针
#### **2. 内存布局**
```
┌─────────────┐
│ 主线程栈 │
│-------------│ ← 仅存储临时变量和返回地址
│ Future对象指针 →
├─────────────┤
│ 堆内存 │
│-------------│ ← Future对象包含所有协程状态
│ a=1 │
│ io_future │
└─────────────┘
```
#### **3. 性能对比**
| 指标 | 共享栈协程 | 无栈协程 |
|---------------------|----------------|----------------|
| 单次切换开销 | ~35ns | ~18ns |
| 内存开销/协程 | 2KB-8KB | 200B-2KB |
| 最大协程数 (8GB) | ~1,000,000 | ~10,000,000 |
| 局部变量访问 | 直接栈访问 | 间接堆访问 |
---
### **三、关键问题解答**
#### **Q1协程调用是否需要切换栈指针**
- **共享栈协程****需要**显式切换`rsp`寄存器
```asm
; Go调度器切换goroutine时的关键操作
MOVQ gobuf_sp(BX), SP // 加载新协程的栈指针
```
- **无栈协程****无需切换**,所有状态在堆上管理
#### **Q2如何避免栈数据覆盖**
- **共享栈**:物理内存隔离,每个协程栈独立分配
```c
// Go连续栈扩容时的复制操作
old_stack = current_stack;
new_stack = malloc(new_size);
memcpy(new_stack, old_stack, old_size);
free(old_stack);
```
- **无栈协程**:局部变量存储在堆对象中,与调用链解耦
#### **Q3为何无栈协程性能更高**
- **寄存器压力小**:无需保存/恢复完整寄存器上下文
- **缓存友好**:状态机结构通常更紧凑,提高缓存命中率
- **零内存拷贝**:状态切换通过指针跳转完成
---
### **四、语言实现对比**
#### **1. Go (共享栈)**
- **栈增长策略**
- 初始2KB每次翻倍直至32KB之后按25%增长
- 栈收缩机制防止内存泄漏
- **调度优势**
```go
// Go调度器在系统线程间均衡goroutine
runtime.GOMAXPROCS(4) // 使用4个OS线程
```
#### **2. Rust (无栈)**
- **零成本抽象**
```rust
// async函数编译为状态机无额外运行时开销
let fut = async { 1 };
// 等效于:
struct MyFuture { /* 状态字段 */ }
impl Future for MyFuture {
type Output = i32;
fn poll(...) { ... }
}
```
- **无堆分配优化**
```rust
// 通过`#[async_trait]`实现动态分发
#[async_trait]
trait MyTrait {
async fn method(&self);
}
```
#### **3. Python (生成器式无栈)**
- **生成器协程**
```python
async def coro():
await asyncio.sleep(1)
# 转换为:
class Coro:
def __init__(self):
self.state = 0
def __next__(self):
if self.state == 0:
self.state = 1
return sleep(1)
```
- **缺陷**:无法深度递归(受解释器栈限制)
---
### **五、总结**
- **共享栈协程**:通过独立栈空间和`rsp`切换实现真并发适合需要深度递归或复杂调用链的场景以Go为代表。
- **无栈协程**通过状态机和堆存储实现高密度协程以Rust/Python为代表适合I/O密集型和高并发场景。
- **选择依据**
- 需要**高性能和可控内存** → Rust无栈协程
- 需要**简单并发模型和深度调用** → Go共享栈协程
- **快速开发和高层抽象** → Python无栈协程
两种模型各有优劣,现代运行时(如.NET甚至支持混合模式开发者应根据具体需求选择最合适的协程实现。

View File

@ -0,0 +1,73 @@
---
title: Swift 学习笔记 - 配置 VS Code 开发 Flutter iOS Native 代码
date: 2025-05-15 09:53:53
tags:
- Swift
- iOS
- Flutter
- 编程语言
categories:
- 技术
---
## 背景
这段时间学 Swift 就冲着一个 iOS 开发来的,这学完了必须给环境整上,虽然说 VS Code 和 XCode 搭配着用也没什么不好的,毕竟开发过程中再怎么也没法避免打开 XCode 的过程 (比如配置开发者签名之类的),但是能够写码时全程不离开当前 IDE 终归还是让人感觉会舒服很多,遂研究如何在 VS Code 中配置 Swift 开发环境以及正常开发 Flutter/ios 项目
## 配置 Swift 插件
根据官方提供的文档 [Configuring VS Code for Swift Development](https://www.swift.org/documentation/articles/getting-started-with-vscode-swift.html),可以在 VS Code 插件商店中下载 Swift 插件,即可开始编写一般的 Swift 项目了
{% note warning flat %}
在 Windows 上Swift 插件可能会因为编码原因拒绝工作,例如出现错误 `Unable to parse output from 'swift package init --help'`,目前在本地未能解决,建议直接使用 ssh 远程 Mac 机器进行开发
{% endnote %}
安装完插件后,对于一般的普通 Swift 项目,插件看起来能够正常工作,包括 F5 运行等都可以正常使用。似乎编译器的依赖关系是通过 `Package.swfit` 解析的,正确配置 `Package.swift` 即可在一般项目里正常开发
## No such module 'Flutter'
创建完 Flutter 工程后,进入 `ios` 文件夹下 Flutter 创建的 swift 文件,会发现报错 `no such module 'Flutter'`,此时如果使用 XCode 打开同样会有此报错
由于是初始工程,没有添加任何使用了 native code 的插件,因此在 `ios` 文件夹下是**不会有 Podfile 文件的**,网上关于这个问题的解决方法大多是 `pod install`,对当前的问题没有任何效果,可以不用尝试了
误打误撞发现在 XCode 下选择 Product-Analyze 后XCode 内报错消失,猜测是 XCode 成功找到了 Flutter 相关的库文件,然而 Swift 插件由于缺少 `Package.swift` 文件,依然无法找到 Flutter 模块
遂就此思路询问 Gemini借助大模型强大的 DeepResearch 能力,得到了下述解决方案
### 安装 SweetPad
首先,在 VSCode 插件商店中搜索 SweetPad 插件并安装,安装完成后左侧工具栏会出现糖果图标
点击糖果图标进入 SweetPad 配置页面,大概率 SweetPad 已经自动识别了工程。如果没有,可能需要在 `.vscode/settings.json` 中手动添加配置:
```json
{
"sweetpad.build.xcodeWorkspacePath": "ios/Runner.xcworkspace"
}
```
之后重新加载 VS Code
### 安装 xcode-build-server
使用 brew 安装 xcode-build-server:
```bash
brew install xcode-build-server
```
如果想的话,可以额外安装 xcbeautify (SweetPad 推荐):
```bash
brew install xcbeautify
```
### 生成 buildServer.json
打开命令面板 (或使用 `Ctrl` + `Shift` + `P` / `Command` + `Shift` + `P`),搜索 `SweetPad`
选中 **SweetPad: Generate Build Server Config**
SweetPad 应该会提示选择对应的 scheme例如 `Runner` 以及目标设备等
完成后SweetPad 应该会生成例如 `buildServer.json` 或是 `compile_commands.json` 这样的文件,之后重新启动 VS Code 即可

View File

@ -0,0 +1,606 @@
---
title: Swift 学习笔记 - 从 Property Wrapper 视角探索 State 与 Binding 如何工作
date: 2025-05-12 23:30:52
tags:
- Swift
- iOS
- 编程语言
categories:
- 技术
---
## 背景
基本学完 Swift 语法后,就开始跟随 [Apple 关于 Swift UI 的官方教程](https://developer.apple.com/tutorials/swiftui-concepts) 来学习 Swift 如何应用在 iOS App 开发中。
在 [Driving Changes in your UI with State and Bindings](https://developer.apple.com/tutorials/swiftui-concepts/driving-changes-in-your-ui-with-state-and-bindings) 这一小节中,教程首次引入了 Swift UI 中关于状态的概念。
教程中使用了简单的代码来介绍 `@State``@Binding` 属性,以及如何在工程中使用这两个属性:
```swift
import Foundation
// 省略部分代码 ...
struct RecipeEditorConfig {
var recipe = Recipe.emptyRecipe()
var shouldSaveChanges = false
var isPresented = false
// 省略部分代码 ...
}
```
```swift
import SwiftUI
struct ContentListView: View {
// 省略部分代码 ...
@State private var recipeEditorConfig = RecipeEditorConfig()
var body: some View {
// 省略部分代码 ...
RecipeEditor(config: $recipeEditorConfig)
// 省略部分代码 ...
}
// 省略部分代码 ...
}
```
```swift
import SwiftUI
struct RecipeEditor: View {
@Binding var config: RecipeEditorConfig
var body: some View {
NavigationStack {
RecipeEditorForm(config: $config)
// 省略部分代码 ...
}
}
// 省略部分代码 ...
}
```
由于之前也接触过其他的前端语言,例如 Dart 或是 JavaScript自然地就会好奇Swift 这样的语法设计下,是怎么实现状态的刷新的呢?
## 一些猜测
比较直接的猜测就是,传入的 `$config` 类似闭包语法,实则是传递了一个 `{ config in return config }` 闭包给 `RecipeEditor`
这看起来好像解答了为什么 `RecipeEditor` 可以直接修改 `ContentListView``recipeEditorConfig` 的值,但实则有几个关键的问题没有解答:
1. `@State` 存在的意义是在状态变更的时候通知渲染层进行重绘,类似 `setState` 方法,如果直接传入变量闭包,虽然可以修改,但是如何通知 UI 层重绘?
2. 如果传入的是闭包,闭包是如何赋值给 `RecipeEditorConfig` 这样的变量的?为什么没有 `init` 函数依然可以完成这样的赋值?
3. 在 `RecipeEditor` 中,是可以直接修改 `config` 的值的,例如直接给 `config` 赋新值,如果原本的引用通过闭包传入后解析出来赋值给 `config`,那这样修改后不就丢失了对原先的对象的引用吗?
几番提问下来,基本可以肯定应该不是直接通过闭包传递的,而是 `@State``@Binding` 做了更复杂的处理
而具体做了哪些处理,则要从 Property Wrapper 开始讲起
## Swift 中的 Property Wrapper
在一篇关于 `@State``@Binding` 的问答中,我了解到,这两个属性并非宏 (至少不是库中按照 Macro 描述编写的那种宏),而是受语言支持的两个 Property Wrapper
关于 Property Wrapper 的内容,在 [Swift Evolution - Property Wrapper](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0258-property-wrappers.md) 中有提到整个 Property Wrapper 设计的背景、提案和应用场景
简单说来Property Wrapper 可以分为 3 步:
1. 编写由 `@propertyWrapper` 标识的结构体,这个结构体用于存储包装属性所需要的所有额外信息以及可以提供的方法(例如一个 lazy 包装属性可能会想要存储当前变量是否已经被初始化过、初始化后的值等),这个结构体会暴露 `wrappedValue``projectValue` 两个属性供外部访问
2. (猜想): 编译器识别到 `@propertyWrapper`,将后续结构体对应的 AST 转化为包含该结构体定义的,更复杂的宏定义
3. 编译器识别到 `@MyPropertyWrapper`,将后续对应的声明 AST 转化为更复杂的表达式,包括一个私有 `MyPropertyWrapper<Value>` 结构体对象和对应的访问器
### PropertyWrapper 结构体
Property Wrapper 这一方案的提出意图解决的问题就是,当访问一个变量时,能够在不增加越来越多且复杂的编译器属性的同时,支持将通用的、可复用的额外逻辑作用在这个访问过程中
例如,关键词 `lazy` 本质上就是在试图解决这类问题中的一个: 当访问一个变量时,先判断是否初始化,如果初始化则返回初始化后的值,否则进行初始化
最初的想法可能就是,写一些重复的代码 (boilerplate code) 来实现这个功能:
```swift
// Irrelevant code omitted
struct MyClass {
var _myDefaultValue: Int = 123
var _myValue: Int? = nil
var myValue: Int {
get {
if _myValue == nil {
_myValue = _myDefaultValue
}
guard let v = _myvalue else {
raise fatalError()
}
return v
}
set {
_myValue = newValue
}
}
}
```
但是如果每一个需要用到这个逻辑的地方都要这么些,未免有些复杂
考虑到宏可以扩展代码,也许可以定义一个宏 `@Lazy` 来展开这些内容,从而避免每次都重复编写
```swift
// Irrelevant code omitted
struct MyClass {
// 也就是,将
@Lazy var myValue: Int = 123
// 转换为
var _myValue: Int? = nil
var myValue: Int {
get {
if _myValue == nil {
_myValue = 123
}
guard let v = _myvalue else {
raise fatalError()
}
return v
}
set {
_myValue = newValue
}
}
}
```
然而,具体的宏想想就已经非常复杂了
也就是说,如果想要直接用宏来实现给属性访问添加额外逻辑这一能力,虽然可以做到,但那样的话似乎更多的关注会在如何实现宏本身而不是实际的逻辑上了 (Make 的困境幻视)
那么,从语言设计的角度考虑,是不是可以让开发者更关注于具体逻辑的实现而隐藏宏转换的细节呢?类似 CMake 那样,用一套规范来约束开发者对逻辑的定义,再在这一规范之上构建编译器属性,使得能够将具体逻辑转换成某种编译器能识别的宏,从而开发者只需要遵循规范来定义逻辑,而不再需要编写具体的宏定义了
虽然不知道 Swift 具体是怎么实现的,但我想 Property Wrapper 的出现大概率是相似的思路
在 Property Wrapper 的描述中Swift 约定了一个编译器属性 `@propertyWrapper`,其可以用于修饰自定义的结构体,结构体的一般结构约定如下:
```swift
@propertyWrapper
struct MyPropertyWrapper {
private var myPropertyUnderlyValue: MyValueType
init(myPropertyUnderlyValue: MyValueType) {
self.myPropertyUnderlyValue = myPropertyUnderlyValue
}
var wrappedValue: MyValueType {
get {
// Add logic when get
myPropertyUnderlyValue
}
set {
// Add logic when set
myPropertyUnderlyValue = newValue
}
}
var projectedValue: SomeProjectedType {
get {
self
}
}
}
```
我想,之所以这么设计,是因为通过 `struct` 可以提供一个统一的抽象语法树节点来描述整个额外逻辑所需要的所有内容。想象如果不使用 `struct` 结构来包装,那为了实现 `@Lazy` 的能力,需要找一个地方写 `_myValue` 变量,另找一个地方写 `myValue` 需要的 `getter``setter` 逻辑,并且编译器要能够直到这些分散的表达式节点都是 `@Lazy` 的一部分,这想想就不好实现
通过 `struct` 的包装,虽然使得实际值的存储多了一层结构体的封装,但简化了编译器的实现,并且也统一了可能的表现形式
现在,编译器只需要在将这个结构体转换为一个宏,其在标记了对应属性的声明处进行如下操作即可:
1. 将原先的声明替换为一个 `_originalVarName: MyPropertyWrapper` 的值
2. 声明 `var originalVarName: MyPropertyWrapper { get, set }` 访问器,通过 `MyPropertyWrapper.wrappedValue` 进行访问
3. 声明 `var $originalVarName: SomeProjectedType { get, set }` 访问器,通过 `MyPropertyWrapper.projectedValue` 进行访问
4. 生成 synthesized initializer (如果必须)
看起来就很好实现了,不是吗?
最后生成出来的代码大致可以理解成下面这样:
```swift
// Original class declaration
// struct MyClass {
// @MyPropertyWrapper var myPropertyValue: MyValueType
// }
// Converted class declaration
struct MyClass {
private var _myPropertyValue: MyPropertyWrapper
var myPropertyValue: MyValueType {
get {
_myPropertyValue.wrappedValue
}
set {
_myPropertyValue.wrappedValue = newValue
}
}
var $myPropertyValue: SomeProjectedType {
get {
_myPropertyValue.projectedValue
}
set {
_myPropertyValue.projectedValue = newValue
}
}
init(myPropertyValue: MyValueType) {
_myPropertyValue = MyPropertyWrapper(myPropertyUnderlyValue: myPropertyValue)
}
}
```
### projectedValue 和 wrappedValue
`MyPropertyWrapper` 的实现中,`wrappedValue` 的作用比较显而易见: 提供了一个对实际存储内容的访问器,在访问器中实现了需要对目标属性添加的额外访问逻辑。而通过将 `myPropertyValue` 改为对 `_myPropertyValue.wrappedValue` 的访问器,可以满足在当前类内对目标属性的访问经过所需要的额外逻辑
然而,只暴露 `wrappedValue` 往往不能满足需求,因为在类内访问 `self.myPropertyValue` 会直接经过 `_myPropertyValue.wrappedValue.get` 解析到 `_myPropertyValue.myPropertyUnderlyValue` 这一实际的底层存储值,这对变量传递就不太友好了: 如果我试图将这个属性传递到其他的函数中,它就会作为实际存储值进行传递,而不会再传递对应的访问器了!
虽然 `@autoclosure` 看似可以解决这个问题但是这需要被调函数进行主动适配显然不能够满足所有的情况。我猜想Swift 语言的开发团队就是为此类情况而额外支持了一种访问器: `projectedValue`
`projectedValue` 可以由开发者自由指定需要返回的对象,并且在类内以 `$myPropertyValue` 的形式暴露,一个简单的设计就是:
```swift
var projectedValue: MyPropertyWrapper {
self
}
```
提供一个对 `MyPropertyWrapper` 实例对象的 `get` 访问器,从而当使用 `$myPropertyValue` 进行传参时,依然可以通过传入参数的 `wrappedValue` 来访问并且修改底层存储值
## State 的魔法
在 SwiftUI 中,`@State` 就是依赖了 Property Wrapper 这一语言特性
很显然,对于显示内容状态的存储,完全符合了 Property Wrapper 的适用场景:
- View 的数据需要以成员变量形式存储并且加以访问和修改 (对象为类的成员变量)
- 对 View 所引用数据的修改需要通知 UI 框架进行重绘 (变量需要在被赋值时添加额外的通知逻辑)
- 数据需要能够沿着控件树向下传递,并且子树也能够对存储数据进行修改 (包含额外逻辑的变量访问器需要能够作为参数传递)
- 对于所有的 View 需要的数据,虽然类型不同,但依赖的逻辑相同 (需要多处复用的额外逻辑)
### 可能的 State 结构体
要实现对状态的存储和访问,`@State` 包装主要需要做的就是在变量被赋值时,触发 UI 更新逻辑
因此,一个可能的简化版的 `@State` 大概会是如下的样子:
```swift
@propertyWrapper
struct State<T> {
private state: T
var wrappedValue: T {
get {
state
}
set {
state = newValue
// call UI update logic
}
}
var projectedValue: State<T> {
get {
self
}
}
init(state: T) {
self.state = state
}
}
```
### 展开后的 @State 代码
参考 Property Wrapper 展开的逻辑,对于使用了 `@State` 的成员变量,其简化后的展开代码大概会是下面的样子:
```swift
class MyView: View {
// Original declaration
// @State var counter: Int = 5
// Expanded declaration
var _counter: State<Int> = State(5)
var counter: Int {
get {
_counter.wrappedValue
}
set {
_counter.wrappedValue = newValue
}
}
var $counter: State<Int> {
get {
_counter.projectedValue
}
}
var body: some View {
// View hierarchy
}
}
```
对于当前的 View 来说,直接使用例如 `counter = counter + 1` 可以更新状态值并且触发 UI 更新逻辑;而通过传递 `$counter` 则可以允许其他 View 通过 `State<Int>` 提供的接口来更新状态值并且触发 UI 更新逻辑
### 使用 Binding 作为 projectedValue
这看起来已经解决了大半的问题,但还有一些情况需要考虑: 例如有时可能会需要将已有的属性外再添加一些逻辑后再传递给子控件
对于这种情况,如果直接传递 `$state` 所对应的 `State<Int>` 对象,显然是不满足要求的,而如果只是传递一个 `@autoclosure`,又没办法实现 `set` 能力
因此,可以想到的就是创建一个额外的类,来包含需要新增的逻辑,并提供访问接口 (就像另一个 Property Wrapper 那样):
```swift
class WierdCounter {
let get: () -> Int
let set: (Int) -> Void
init(get: () -> Int, set: (Int) -> Void) {
self.get = get
self.set = set
}
var wrappedValue: Int {
get {
self.get()
}
nonmutating set {
self.set(newValue)
}
}
}
class MyView: View {
@State var counter: Int = 5
var wrappedCounter: WierdCounter = WierdCounter {
counter
}, set: { newValue in
counter = newValue * 2
}
var body: some View {
MyAnotherView(counter: wrappedCounter)
}
}
```
此时,更进一步地思考,目前子控件想要访问父控件传入的状态的话,多少还是有些复杂的: 不仅有时候传入 `State<T>` 有时候传入额外的类,访问实际状态的时候还要经过额外的一层访问器去访问
相比 Swift 设计团队也是这么想的,于是他们首先解决了第一个问题: 先让传入的状态参数类型统一
这其实还比较好实现,不难发现,`WierdCounter``State<T>` 其实有着相似的结构,而且他们的核心目标都是需要访问 `counter.wrappedValue`,因此不妨在库中就提供一个类似的类,例如 `MyBinding<T>`:
```swift
struct MyBinding<T> {
let get: () -> T
let set: (T) -> Void
var wrappedValue: T {
get {
self.get()
}
nonmutating set {
self.set(newValue)
}
}
init(@escaping get: () -> T, @escaping set: (T) -> Void) {
self.get = get
self.set = set
}
}
```
对应的,`State<T>` 的设计也进行相应的调整:
```swift
@propertyWrapper
struct State<T> {
private state: T
var wrappedValue: T {
get {
state
}
set {
state = newValue
// call UI update logic
}
}
var projectedValue: MyBinding<T> {
MyBinding {
self.wrappedValue
}, set { newValue in
self.wrappedValue = newValue
}
}
init(state: T) {
self.state = state
}
}
```
这样,展开后的 `MyView` 类也会有相应的变化:
```swift
class MyView: View {
// Original declaration
// @State var counter: Int = 5
// Expanded declaration
var _counter: State<Int> = State(5)
var counter: Int {
get {
_counter.wrappedValue
}
set {
_counter.wrappedValue = newValue
}
}
var $counter: MyBinding<Int> {
_counter.projectedValue
}
var body: some View {
// View hierarchy
}
}
```
统一了状态传参的类型,接下来,对于第二个问题,就是 `@Binding` 展现魔法的时刻了
## Binding 的魔法
其实读到这里,难免会有一种不由自主的冲动:
既然状态参数的类型都统一了,那要简化子控件访问状态的逻辑,不就是要打包 `incomingState.wrappedValue` 这个逻辑嘛!让每个对 `incomingState` 的访问都以 `incomingState.wrappedValue` 的方式进行,不就可以了嘛?
没错!还记得 Property Wrapper 就是干这事的吧!
想必 Swift 开发团队也是这么想的,于是就有了 `@Binding`
### 可能的 Binding 结构体
有了 `@State` 的经验,猜想 `@Binding` 的实现就简单多了
不妨猜测简化后的 `@Binding` 如下:
```swift
@propertyWrapper
struct Binding<T> {
let _binding: MyBinding<T>
let wrappedValue: T {
get {
self._binding.wrappedValue
}
nonmutating set {
self._binding.wrappedValue = newValue
}
}
let projectedValue: Binding<T> {
self
}
init(@escaping get: () -> T, @escaping set: (T) -> Void) {
self.get = get
self.set = set
}
}
```
这看起来和 `MyBinding<T>` 的定义也太像了!不妨试试合二为一:
```swift
@propertyWrapper
struct Binding<T> {
let get: () -> T
let set: (T) -> Void
let wrappedValue: Binding<T> {
get {
self.get()
}
nonmutating set {
self.set(newValue)
}
}
let projectedValue: Binding<T> {
self
}
init(@escaping get: () -> T, @escaping set: (T) -> Void) {
self.get = get
self.set = set
}
}
```
太神奇了,这样甚至不知不觉中统一了 `Binding<T>``MyBinding<T>` 的实现!接下来,只要让编译器生成 `synthesized initializer` 的时候适配 `Binding<T>` 的初始化方式 (使用 `get``set` 参数而不是 `Binding<T>` 对象) 似乎就完成了
### 展开后的 @Binding 代码
那么,根据 Property Wrapper 的展开逻辑,我们来推测一下 `@Binding` 展开后的代码:
```swift
struct MySubView: View {
// Original declaration
// @Binding var parentState: Int
// Expanded declaration
var _parentState: Binding<Int>
var parentState: Int {
get {
_parentState.wrappedValue
}
set {
_parentState.wrappedValue = newValue
}
}
var $parentState: Binding<Int> {
get {
_parentState.projectedValue
}
}
// Synthesized initializer
init(parentState: Binding<Int>) {
self._parentState = Binding {
parentState.wrappedValue
}, set: { newValue in
parentState.wrappedValue = newValue
}
}
}
```
## 一些奇怪的问题记录
至此,基本完成了对 Swift 中 `@State``@Binding` 实现原理的深入探索了,但难免还会有一些未解的疑惑
感谢如今大模型的能力,使得部分奇怪的问题得以解答
以下列出部分与大模型问答的摘要
### 手动指定 initializer
#### 提问
如果 `@State``@Binding` 的生成涉及 `synthesized initializer` 的生成,那么如果我自己手动指定了 initializer是否与前述中自动生成的 initializer 产生冲突?
#### 回答
会产生影响,如果手动指定了 initializer需要手动添加对应的初始化逻辑例如:
```swift
init(parentState: Binding<Int>, someOtherParams: MyType) {
// Add the initialization of bindings
self._parentState = Binding {
parentState.wrappedValue
}, set: { newValue in
parentState.wrappedValue = newValue
}
// Other logic
}
```

View File

@ -284,7 +284,7 @@ aside:
enable: true
icon: fab fa-github
text: Follow Me
link: https://github.com/xxxxxx
link: https://github.com/Linloir
card_announcement:
enable: true
content: This is my Blog