在 Unity 中,定时任务通常使用 coroutine(协同程序,即协程)来实现;自己写 Timer 或使用 Invoke 也能达到同样效果。本文主要分析 Unity 中 coroutine 的实现原理。
以 WaitForSeconds 为例,coroutine 内部会将迭代结果添加到 CallDelay 队列,交由 DelayedCallManager 统一管理。该管理器的内部存储结构是一个 multiset(为什么不用 map?),定时器的触发条件是 CallBack 的时间到了——它依赖时间戳进行调度,而不是每帧 Tick 一次。排序键就是 CallBack 的 time 值。
使用非顺序容器(物理上无序)的好处是:在遍历过程中可以动态地增删元素,而不影响整体排序。增删代价都限制在很小的范围内,不会像 vector 那样引发大规模数据前移。基于稳定排序,multiset 中排序值相同的元素始终按插入顺序排列(即相同时间戳时保持添加顺序)。如果排序结果不稳定,就会出现 BUG:coroutine 的调用顺序在某些情况下不会按 StartCoroutine 的先后顺序执行。
因此,用 StartCoroutine 来做定时任务并没有性能优势,更多是写法上的优雅,所以不建议大量使用。
示例代码(TestCode)
内部再对 WaitForSeconds 添加到定时器任务,从而达到协同程序的目的。
发起添加一个定时器 CallBack 时,coroutine 内部在获得 IEnumerator 之后,同样是调用 calldelayed 来完成注册。
如果延迟时间是 0.0f,当前帧的 Update 不处理,下一次 Update 才处理,此时 time = -1f。