Unity的协程实现一直困扰着我,花了很多时间去翻资料也没看出个所以然,今天又去查阅相关博客,总算有个比较清晰的理解,记录一下。
想要弄清楚Coroutine的机制,就必须先弄清楚什么是IEnumerator,IEnumeratable,yield。
IEnumerator和IEnumeratable:
这两个接口的代码非常简单,但是理解起来其实也需要一点思路。先贴出它们的代码:
public interface IEnumerator
{
object Current {get:}
bool MoveNext();
void Reset();
}
public interface IEnumeratable
{
IEnumerator GetEnumerator();
}
实现IEnumerator的类就可以进行foreach遍历,这个很好理解,但是为什么一个类要能够遍历,除了要继承IEnumerator之外还要继承IEnumeratable呢?这里我觉得C#之所以这样设计,是为了体现面向对象的思想。IEnumerator可以理解为怎么实现枚举的方法,IEnumeratable可以理解为一个对象是否可枚举。
yield:
yield这个关键字,我觉得是C#里面最难理解的关键字。对于yield的理解一直都很模糊,不知道大家是不是和我一样的状况。今天看到下面这段代码,不仅对Coroutine的理解加深,也对yield的作用有进一步认识。代码如下:
class Test
{
static IEnumerator GetCounter()
{
for (int count = 0; count < 10; count++)
{
yield return count;
}
}
}
很简单的代码段,但是看起来很奇怪,count明明是int类型,怎么可以作为IEnumerator的结果进行return呢?之所以会让人觉得奇怪,就是因为这里的yield关键字,让我们看下编译后的代码:
internal class Test
{
// Note how this doesn't execute any of our original code
private static IEnumerator GetCounter()
{
return new <GetCounter>d__0(0);
}
// Nested type automatically created by the compiler to implement the iterator
[CompilerGenerated]
private sealed class <GetCounter>d__0 : IEnumerator<object>, IEnumerator, IDisposable
{
// Fields: there'll always be a "state" and "current", but the "count"
// comes from the local variable in our iterator block.
private int <>1__state;
private object <>2__current;
public int <count>5__1;
[DebuggerHidden]
public <GetCounter>d__0(int <>1__state)
{
this.<>1__state = <>1__state;
}
// Almost all of the real work happens here
private bool MoveNext()
{
switch (this.<>1__state)
{
case 0:
this.<>1__state = -1;
this.<count>5__1 = 0;
while (this.<count>5__1 < 10) //这里针对循环处理
{
this.<>2__current = this.<count>5__1;
this.<>1__state = 1;
return true;
Label_004B:
this.<>1__state = -1;
this.<count>5__1++;
}
break;
case 1:
goto Label_004B;
}
return false;
}
[DebuggerHidden]
void IEnumerator.Reset()
{
throw new NotSupportedException();
}
void IDisposable.Dispose()
{
}
object IEnumerator<object>.Current
{
[DebuggerHidden]
get
{
return this.<>2__current;
}
}
object IEnumerator.Current
{
[DebuggerHidden]
get
{
return this.<>2__current;
}
}
}
}
仔细地看上面这段代码,是不是有点理解了!整个GetCounter()函数被编译成一个class!yield之所以让人觉得很奇怪,我想就是怪在这里,不看实现的话,根本就不会知道,也不会想到一个函数会被编译成一个类。让我们再仔细看看上面的代码给了我们什么信息,GetCounter()可以看成一个平常的类,<>2__current就是yield return 的count,this.<>1_state我把它理解为断点位置,GetCounter()里面有多少行yield return 的语句,this.<>1_state就有多少个状态,反应在switch结果里面就是有多少个case。
Coroutine:
Unity的Coroutine很多资料都讲了,不是线程,它其实也是由运行在Unity引擎下的MainThread线程调用。和Uptate,FixedUpdate这些MonoBehaviour的函数类似,都是由Unity引擎在每帧检查,在适当的时机进行调用的函数。
StartCoroutine():
在调用StartCoroutine("CoroutineFun")的时候,Unity做了什么呢?可以这样理解,Unity的每一个MonoBehaviour对象,都会保存一个协程栈,调用StartCoroutine时,CoroutineFun便添加进协程栈里,之后每一帧检测协程栈里每一个协程的状态,如果该协程当前可执行,就MoveNext,已经执行完了的,就移除。
yield return new WaitForSecond(2)
这个语句可以这样理解,Unity在判断协程是否可以调用MoveNext前,会先判断迭代器的object Current值,如果Current的类型是WaitForSecond,那么就判断当前时间是否已经过了2秒,如果是就调用迭代器的MoveNext,如果还没过2秒,就跳过。其他的yield return null语句,用迭代器的原理去理解就可以了。
现在我们把这些知识点串联起来:Unity协程的实现,是基于C#的迭代器,开启一个协程其实就是把一个迭代器放进栈里,然后每帧去判断迭代器的<>2_current和<>_1state是否满足条件,满足条件就执行MoveNext(),直到迭代结束。
基本就是这些内容,有一些是自己的理解,没有仔细去翻看源代码,希望是对的,哈哈。也希望能帮到大家,谢谢!