看了很多关于Unity 协程的文章,自己来总结一下.

Unity 中的什么周期

链接:https://docs.unity3d.com/Manual/ExecutionOrder.html

上图其实可以看到,yield waitforEndOfFrame是在帧结束的时候,WaitForFixedUpdate是在所有物理更新结束之后,其他的都在update之后,LateUpdate之前更新.

Unity协程的本质

Unity中协程并不是线程,只是Unity中的另一条逻辑,规定了相应的时间段然后响应某些事件.协程内部其实是一个迭代器. 如上面的生命周期,每一帧都会检测协程是否满足迭代器中MoveNext的条件,当满足的时候 就回去执行下一个条件,当没后面没有 那么就协程结束.例如下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
private IEnumerator Start()
{
while (true)
{
Debug.Log("Coroutine...");
yield return null; //我这yield return 放到后面
}
}
private void Update()
{
Debug.Log("Update...");
}

打印结果

可以看到一开始是协程优先,然后是update,可是后面总是update更新后才执行协程.

不管我们使用WaitForSeconds WaitForSecondsRealtime或者其他都是在每一帧中进行检查,如果满足迭代器中的MoveNext()就会向下执行.

我们再看看WaitForSeconds (其他也类似)其实这个类只是记住了一个时间点,通过ILSpy,我们可以看到其代码实现很简单

内部维护了一个时间节点,用于在每一帧检测当前时间点与开始的时间点是否是规定的时间差,那么在执行MoveNext

引用一下别人的话

提示下:YieldInstruction是一个类 不是一个接口

协程必须使用StartCoroutine,来启动,通过这个函数,创建了Coroutine对,该对象保存了yield return展开后的IEnumerator对象指针、MoveNextCurrent的函数指针、结束后应当唤醒的协程的指针、指向调用者Monobehaviour的指针等等,并将该对象coroutine保存到该Monobehaviour的活跃协程列表中。然后立即调用了coroutine.Run()coroutine.Run()首先尝试调用InvokeMoveNext,若发现当前协程执行完成,则会尝试调用应当唤醒的协程,否则才真正执行MoveNext,获得返回值monoWait


根据返回值monoWait的类型,进行不同的处理。通常是传递不同的参数给CallDelayed函数。对于返回值是Coroutine类型(c#那边用了协程嵌套),会将这个返回值的结束后应唤醒的协程的指针指向当前的coroutine。笔者这里发现了一种不太常见的用法:当返回值为IEnumerator类型(c#那边没有用StartCoroutine去开启嵌套协程,而是直接在yield return 后调用)时,Unity会自动为其创建一个Coroutine对象并初始化,效果同样。


CallDelayed函数传入了运行协程对象的方法、qingli协程对象的方法、清理条件等。函数内部创建了一个Callback对象,加到了全局的DelayedCallManager的列表中。游戏主循环会在每一帧调用DelayedCallManager.Update,在满足一定条件时(比如对应的Monobehaviour对象还没被销毁等)调用Callback对象的方法。

所以,事情其实也就变的简单了,通过StartCoroutine来启动一个协程,提交到Unity内部更新机制中,每一帧来检测是否满足设定的条件,执行迭代器中的MoveNext

When you make a call to StartCoroutine(IEnumerator) you are handing the resulting IEnumerator to the underlying unity engine.

StartCoroutine() builds a Coroutine object, runs the first step of the IEnumerator and gets the first yielded value. That will be one of a few things, either “break”, some YieldInstruction like "Coroutine", "WaitForSeconds", "WaitForEndOfFrame", "WWW", or something else unity doesn’t know about. The Coroutine is stored somewhere for the engine to look at later.

… At various points in the frame, Unity goes through the stored Coroutines and checks the Current value in their IEnumerators.


WWW
- after Updates happen for all game objects; check the isDone flag. If true, call the IEnumerator’s MoveNext() function;

WaitForSeconds
- after Updates happen for all game objects; check if the time has elapsed, if it has, call MoveNext();

null or some unknown value
- after Updates happen for all game objects; Call MoveNext()

WaitForEndOfFrame
- after Render happens for all cameras; Call MoveNext

MoveNext returns false if the last thing yielded was “break” of the end of the function that returned the IEnumerator was reach. If this is the case, unity removes the IEnumerator from the coroutines list.

协程中的优化

协程其实不会很占用资源 GC,只是平时要注意写法,如果是如下的写法 那么就很坑了

1
2
3
4
5
6
7
8
9
private IEnumerator Start()
{
while (true)
{
Debug.Log("Coroutine...");
yield return new WaitForSeconds(0.5f);

}
}

上面 我每次都是要去new 一个waitforSecond,那么就会慢慢产生很多的GC

看下这几个文章

  1. C# 协程 WaitForSeconds产生GC(Garbage Collection)问题 https://blog.csdn.net/u010019717/article/details/44948903

  2. 关于Unity Corotine协程的优化 https://blog.csdn.net/qq_34106090/article/details/88834260

  3. [Unity 协程运行时的监控和优化] https://blog.uwa4d.com/archives/USparkle_Coroutine.html

参考链接

  1. Unity核心原理(1)生命周期 https://zhuanlan.zhihu.com/p/55287195
  2. Coroutine,你究竟干了什么 https://blog.csdn.net/tkokof1/article/details/11842673
  3. C#迭代器——由foreach说开去 https://blog.csdn.net/u013477973/article/details/65635737
  4. C#基础知识—迭代器与Foreach语句 https://www.cnblogs.com/3xiaolonglong/p/9608281.html
  5. Unity中关于Coroutine与Async的使用问题 https://www.jianshu.com/p/86779d2ebeaa
  6. Unity 协程原理逆向解析 https://zhuanlan.zhihu.com/p/115172019
  7. 浅析unity/xlua中的协程实现 https://zhuanlan.zhihu.com/p/47573713
  8. 深入剖析Unity协程 https://zhuanlan.zhihu.com/p/82798132