外观
Stage02|Engine Loop
Lesson015|Scheduler 和 ComponentScheduler 分别负责什么?
Tags: #Creator2.x #Stage02 #Scheduler #ComponentScheduler #TimerDifficulty: ⭐⭐⭐⭐☆
一、本课目标
本课回答:
引擎如何在每一帧找到“现在应该执行的任务”?
重点区分:
- 通用时间调度与组件生命周期调度。
schedule和update的适用场景。- 为什么关闭节点或组件会让任务停止。
- 为什么一个空
update也可能进入每帧调度。
二、两类调度器的职责
可以先用下面的模型理解:
text
Scheduler
├── 定时器
├── schedule / scheduleOnce
├── 优先级和时间间隔
└── 一般性的时间任务
ComponentScheduler
├── Component 注册
├── onLoad / start
├── update / lateUpdate
├── enabled 状态
└── activeInHierarchy 状态Creator 2.x 的具体内部类名和调用细节应以当前引擎源码为准,但职责边界可以稳定地这样记忆:
Scheduler 管时间任务,ComponentScheduler 管组件生命周期和每帧方法。
三、为什么不能只遍历所有 Node
假设场景有 10,000 个 Node,但真正需要每帧更新的只有 300 个组件:
text
直接遍历所有 Node
↓
逐个查找 Component
↓
逐个判断 enabled / active
↓
调用少量 update这会带来大量无意义的遍历。调度系统可以在组件启用、禁用、挂载和销毁时维护可执行集合,减少每帧筛选成本。
这不是说引擎永远不遍历,而是说明“注册时整理状态、运行时快速调度”的设计思路。
四、ComponentScheduler 关注什么
一个组件能否进入更新阶段,至少要满足:
text
Component 已被创建
Component 实现了目标生命周期方法
Component.enabled = true
Node.activeInHierarchy = true
对象没有待销毁当状态改变时,调度系统需要更新组件状态:
text
enabled = true
↓
组件加入可执行集合
enabled = false
↓
组件移出或标记为不可执行
父节点 active = false
↓
子节点 activeInHierarchy 变化
↓
相关组件进入 disable 状态这就是 enabled、activeInHierarchy 和调度集合之间的联系。
五、Scheduler 的时间任务
常见写法:
ts
this.schedule(this.tick, 1);
this.scheduleOnce(this.openPanel, 2);
this.unschedule(this.tick);可以抽象成:
text
注册回调和间隔
↓
每帧接收 dt
↓
累计时间
↓
达到间隔
↓
执行回调
↓
保留或移除任务它不是创建一个操作系统线程,也不是保证回调精确发生在某个毫秒。
text
目标时间到了
↓
下一次引擎有机会检查时执行如果主线程被阻塞,调度回调也不能绕过阻塞立即执行。
六、schedule 和 update 怎么选
适合 update
ts
update(dt: number) {
this.progress += this.speed * dt;
this.refreshPosition();
}适合:
- 每帧连续变化。
- 需要读取本帧状态。
- 移动、跟随、插值和实时控制。
适合 schedule
ts
this.schedule(this.refreshServerTime, 1);适合:
- 低频轮询。
- 周期性刷新。
- 延迟一次执行。
- 不需要每帧精度的任务。
如果一个任务每秒只需要执行一次,就不要写成空转的 update。
七、调度和对象状态的关系
ts
this.schedule(this.tick, 1);
this.node.active = false;这时可能出现的效果是:
text
节点进入非激活状态
↓
组件不再满足正常运行条件
↓
关联调度任务暂停或不再触发不要仅凭函数名判断“定时器一定继续”或“一定停止”。需要看任务注册在哪个对象、对象是否启用、Scheduler 是否全局运行以及当前 Creator 版本的行为。
工程上应明确任务归属:
- UI 倒计时绑定 UI 组件生命周期。
- 网络心跳绑定网络服务生命周期。
- 全局轮询绑定明确的全局管理器。
八、优先级和执行顺序
当多个任务都在同一帧到期时,引擎需要某种顺序规则。常见影响因素包括:
text
组件注册顺序
执行优先级
节点层级
executionOrder
调度器内部队列不要把“Hierarchy 面板上谁在前面”直接当成所有 update 的执行顺序。需要使用引擎支持的执行顺序配置,并尽量减少跨组件的隐式顺序依赖。
更稳妥的设计是:
text
输入采集
→ 状态计算
→ 表现同步把关键阶段显式化,而不是让十几个脚本互相猜谁先执行。
九、调度系统为什么会影响性能
每个可调度组件都会带来:
text
注册成本
状态判断
队列维护
每帧遍历
函数调用因此:
- 不用
update就不要声明空update。 - 低频任务使用
schedule或事件。 - 暂停功能时优先关闭具体组件或任务。
- 列表大量 Item 不要每个都维护无意义的定时器。
- 复杂系统可以由一个管理器集中更新数据。
集中更新不等于所有逻辑塞进一个巨型脚本,而是减少重复调度并保持职责清晰。
十、案例:倒计时列表的两种写法
写法 A:每个 Item 都有 update
ts
update(dt: number) {
this.leftTime -= dt;
this.label.string = formatTime(this.leftTime);
}100 个 Item 就可能产生 100 个每帧调用,即使大部分倒计时显示相同。
写法 B:管理器统一推进
ts
update(dt: number) {
this.clock += dt;
if (this.clock < 1) {
return;
}
this.clock = 0;
this.refreshVisibleItems();
}这并不是所有场景都必须集中更新,而是要根据刷新频率、可见范围和数据依赖选择调度粒度。
十一、常见误区
误区一:Scheduler 就是线程池
Scheduler 通常运行在游戏主线程的时间调度体系中,不等于创建后台线程。
误区二:schedule(1) 会精确每 1000 毫秒执行
它只能在引擎有机会推进时检查时间,主线程阻塞、后台切换和帧率变化都会影响实际触发时间。
误区三:所有 update 都由同一个简单数组遍历
引擎可能维护不同阶段、优先级和状态集合。不要用一个简化模型推断所有源码细节。
误区四:关闭节点后全局任务也一定停止
如果任务归属于全局 Scheduler 或其他管理器,关闭某个节点不一定影响它。
误区五:用更高频 update 解决时间精度
提高业务调用频率不等于提高平台时间精度,反而可能增加 CPU 成本。
十二、练习与答案
练习
- Scheduler 和 ComponentScheduler 的职责分别是什么?
- 为什么不用的脚本不建议写空 update?
- 每秒刷新一次的任务,为什么通常不需要每帧 update?
- 关闭一个节点后,为什么某些全局任务仍可能继续?
- 多个组件依赖执行顺序时,应该如何降低风险?
参考答案
- Scheduler 主要处理时间任务,ComponentScheduler 主要处理组件生命周期和每帧方法。
- 空 update 仍可能进入组件调度、判断和函数调用。
- 使用 schedule 或低频管理器可以减少每帧调度成本。
- 任务可能注册在全局管理器或独立 Scheduler 上,和节点生命周期无关。
- 使用明确的阶段、执行顺序配置或集中管理,避免隐式依赖注册顺序。
十三、Creator 2.4.x 可复现实验:三种调度来源
在同一个节点挂载以下脚本:
ts
const { ccclass } = cc._decorator;
@ccclass
export default class SchedulerProbe extends cc.Component {
private count = 0;
onLoad() {
this.schedule(() => cc.log('[schedule]', ++this.count), 0.5);
this.scheduleOnce(() => cc.log('[scheduleOnce]'), 1.2);
cc.director.getScheduler().schedule(() => cc.log('[global]'), this, 1, cc.macro.REPEAT_FOREVER, 0);
}
update(dt: number) { if (this.count < 2) cc.log('[update]', dt.toFixed(3)); }
onDisable() { cc.log('[disable]'); }
}先运行 3 秒,再把节点设为 inactive,观察哪些日志停止;最后在 onDestroy 中取消全局调度并重复切场景。预期组件的 schedule 和 update 随节点/组件状态停止,全局 Scheduler 上注册的任务若未显式取消可能继续执行。若日志数量异常,检查重复 onLoad、重复注册和未对称 unschedule。
十四、版本边界与排查矩阵
this.schedule、unschedule 和 cc.director.getScheduler() 是 Creator 2.4.x 可用的公开入口;ComponentScheduler 的内部队列、优先级和某些重复注册细节应以当前版本源码为准。调度器是主线程时间队列,不会把回调自动移到后台线程。
| 症状 | 证据 | 修复方向 |
|---|---|---|
| 回调越来越多 | 每次打开页面数量递增 | 在 onDisable/onDestroy 对称取消 |
| 节点关闭后仍打印 | 回调注册在全局 Scheduler | 保存 owner,显式 unschedule |
| 低频任务造成卡顿 | 单次回调耗时高 | 拆分任务、降低频率、分帧处理 |
十五、应用题
题目: 一个金币计数器每 0.1 秒刷新一次,但页面不可见时仍消耗 CPU。应如何改?
答案: 让刷新任务归属页面组件,使用 schedule 而不是全局 Scheduler;在 onDisable 取消,在 onEnable 恢复。真正的金币状态由数据层保存,页面重新启用时立即读取一次,避免依赖停留期间的 UI 回调。
十六、本课总结
text
Scheduler 管时间任务。
ComponentScheduler 管组件生命周期和 update。
调度不是线程,主线程阻塞时回调也会延迟。
组件状态变化会影响调度集合。
调度粒度应该和任务频率、可见范围、数据依赖匹配。十七、下一课预告
text
Lesson016|组件生命周期如何注册、启用和移出调度队列?下一课会专门解释组件从创建到进入 update 调度需要经过哪些注册步骤。
