PixelUI 的 Coroutine 是固定 context 加宏的协作式状态机。它适合编排这类短流程
- 带有先后顺序的启动动画
- 需要非阻塞延时的短流程
一个例子
1 | Coroutine intro_{[this](CoroutineContext& ctx) { |
注册与清理
1 | void onEnter(ExitCallback cb) override { |
API 详解
使用这些 API 需要包含:
1 |
01 - CoroutineContext 协程上下文结构体
每个 Coroutine 对象都内置一个 context 上下文结构体,用它保存跨多次调用的执行位置和等待状态:
1 | struct CoroutineContext { |
| 字段 | 含义 |
|---|---|
pc |
宏状态机的执行位置。由 CORO_* 宏维护 |
waitUntil |
CORO_DELAY 使用的绝对时间戳。由宏维护。 |
localData[8] |
提供给业务代码的 8 个持久化 uint32_t 槽位,可在多次 resume 之间保存简单状态。 |
state |
当前状态:CREATED、RUNNING、SUSPENDED 或 FINISHED。 |
waitReason |
挂起原因:NONE、DELAY 或 ANIMATION。调度器据此计算下次唤醒时间。 |
reset() 不会清空 localData。如果一次新的流程不应继承旧值,需要在启动前显式清零所用槽位:
1 | intro_.reset(); |
除 localData 外,不要手动改写 pc、state、waitUntil 或 waitReason,影响协程调度器
02 - Coroutine 对象
构造函数
1 | Coroutine(CoroutineFunction func); |
CoroutineFunction 的实际类型是:
1 | etl::inplace_function<void(CoroutineContext&), CALLBACK_STORAGE_SIZE> |
具体上限见资源上限。
start()
1 | void start(); |
仅当状态为 CREATED 时,将状态改为 RUNNING 并把执行位置设为开头。它不会立即调用 coroutine 函数;函数会在 scheduler 下一次更新时运行。对 RUNNING、SUSPENDED 或 FINISHED 状态调用没有效果。若要重新运行已完成的 coroutine,先调用 reset()。
reset()
1 | void reset(); |
将状态恢复为 CREATED,并重置 pc、waitUntil 和 waitReason。
状态查询
1 | bool isFinished() const; |
isFinished()判断 context 是否为FINISHED。getContext()可用于读取状态或访问localData。优先使用 const 版本进行诊断。
03 - Coroutine 宏
所有宏都必须在同一个 coroutine 函数的
CORO_BEGIN(ctx)与CORO_END(ctx)之间使用。
CORO_BEGIN(ctx) / CORO_END(ctx)
1 | CORO_BEGIN(ctx); |
CORO_BEGIN 宏会部署基于 ctx.pc 的状态机,CORO_END 将协程上下文状态设为 FINISHED 并返回。执行路径要记得有始有终,不要绕过 CORO_END,否则 coroutine 会保持 RUNNING 并被继续调度。
CORO_DELAY(ctx, ui, ms, line)
1 | CORO_DELAY(ctx, ui_, ms, 2); // 挂起协程,等待ms毫秒后恢复 |
此宏记录 deadline(ui.getCurrentTime() + ms),将协程上下文状态设为 SUSPENDED。
到期后从宏的下一行继续。deadline 使用无符号回绕安全比较,但单次等待必须小于 2^31 ms。它会参与 tickless 的最早唤醒时间计算。
CORO_WAIT_ANIMATION(ctx, ui, line)
1 | CORO_WAIT_ANIMATION(ctx, ui_, 3); |
挂起直到 ui.activeAnimationCount()( PixelUI的全部活跃动画 )变为 0。
若其他模块持续创建动画,等待会相应延长。
CORO_YIELD(ctx, line)
用途为让出一帧,实际上基本用不到,是 Duff’s device 风格协程的一个基础原语,调配粒度太粗。基本功能都可以使用 CORO_DELAY 等进行实现
1 | CORO_YIELD(ctx, 1); // 在不进入定时等待的情况下,保存执行位置并立即返回。 |
coroutine 仍为 RUNNING,所以 scheduler 会在下一次处理机会继续执行,而不是在同一次函数调用中继续。它适合把较长流程主动拆成多个短步骤,不能代替基于真实时间的延时。
line 参数规则
line 最终会成为 C++ switch 的 case 标签,因此必须是编译期整数常量,并且在同一个 coroutine 函数内唯一;0 已由 CORO_BEGIN 使用,不能再传。可以使用人工编号,也可以用 __LINE__ 自动取得当前源码行号:
1 | CORO_DELAY(ctx, ui_, 100U, __LINE__); |
由于恢复点是 case 标签,不要让需要跨恢复点存活的普通局部变量依赖栈上的初始化。把持久状态放在 App 成员或 ctx.localData 中:
1 | Coroutine worker_{[this](CoroutineContext& ctx) { |
PixelUI 的协程注册接口
addCoroutine()
1 | void addCoroutine(Coroutine* coroutine); // 将 coroutine 注册到协程管理器 |
把 coroutine 的非拥有指针加入 scheduler。此操作不会调用 start(),也不会检查重复注册。调用前应先 reset()/start(),并确保同一个指针只注册一次。
容量由 PIXELUI_MAX_COROUTINE_NUM 决定;接口没有失败返回值,因此调用方必须在设计上保证不超过固定容量。
对象本身必须活到被移除或 scheduler 清理之后。所以一定要把 Coroutine 作为 App 成员。
removeCoroutine()
1 | void removeCoroutine(Coroutine* coroutine); // 将 coroutine 移出协程管理器 |
移除所有与该指针相同的注册项。它不会改变 coroutine 自身的状态;需要重置效果可以先 reset()、start(),再重新注册。传入未注册的指针不会产生效果。
clearAllCoroutines()
1 | void clearAllCoroutines(); |
清空当前 PixelUI scheduler 中的全部非拥有指针,此操作不会 reset 注册过的协程的上下文。
协程被视为”非持有引用”(non-owning reference),ViewManager 在销毁 App 前会执行全局清理,以避免遗留悬空指针,在 app 切换时统一清理。每个 app 应该在 onEnter() / onResume() 中重新注册自己需要的协程
getActiveCoroutineCount()
1 | size_t getActiveCoroutineCount(); |
返回 scheduler 当前保存的注册项数量,包括
- 等待中的 coroutine
- 被注册但尚未
start()的CREATEDcoroutine。
它是注册数量,完成的 coroutine 会在 scheduler 更新中自动移除。
CoroutineScheduler (协程框架)
1 | explicit CoroutineScheduler(PixelUI& ui); |
更新时 update() 会先清理已完成项,再依次恢复满足条件的 coroutine,并在本轮末尾再次清理刚完成的项。每个 coroutine 执行前都会重新读取全局动画状态,因此前一个 coroutine 新启动的动画会影响后一个 coroutine 的 CORO_WAIT_ANIMATION 判断。nextWakeupMs() 供 PixelUI 的 event-driven/tickless 调度计算最早唤醒时间。


