Coroutine官方解释:
协程是包含可以让出自身执行直到yield指令执行完毕的一个函数。
public Coroutine StartCoroutine(IEnumerator routine)
协程由上面的函数返回, StartCoroutine有一个IEnumerator的参数,从这儿就可以看出, 协程和迭代器脱不开关系。事实上, Coroutine的实现离不开C#的迭代器。
C#的迭代器主要有两个接口, 一个是IEnumerable, 一个IEnumerator, IEnumerable接口只有一个IEnumerator GetEnumerator()的函数, IEnumerator接口有MoveNext(), Reset(), 和 Current三个成分, 其实c#的List, Dictionary等都实现了这两个接口, 也正是通过这两个接口,实现了迭代器的作用,其实IEnumerable可以理解成把IEnumerator包了一层, 通过GetEnumerator函数对同一个对象生成多个互不干扰的IEnumerator迭代器。当我们生命一个对象是一个IEnumerator时, 编译器会自动给我们生成对应的迭代器类,比如说:
IEnumerator Init1()
{
for (int i = 0; i < 5; i++)
{
Debug.LogFormat("init1 enter: {0}, {1}", i, Time.realtimeSinceStartup);
yield return new WaitForSeconds(2); //
}
}
Init1是个简单的协程, 编译器会根据它的定义一个全新的类:
[CompilerGenerated]
private sealed class <Init1>c__Iterator1 : IEnumerator, IDisposable, IEnumerator<object>
{
internal int <i>__1;
internal object $current;
internal bool $disposing;
internal int $PC;
object IEnumerator<object>.Current
{
[DebuggerHidden]
get
{
return $current;
}
}
object IEnumerator.Current
{
[DebuggerHidden]
get
{
return $current;
}
}
[DebuggerHidden]
public <Init1>c__Iterator1()
{
}
public bool MoveNext()
{
uint num = (uint)$PC;
$PC = -1;
switch (num)
{
case 0u:
<i>__1 = 0;
goto IL_008a;
case 1u:
<i>__1++;
goto IL_008a;
default:
{
return false;
}
IL_008a:
if (<i>__1 < 5)
{
Debug.LogFormat("init1 enter: {0}, {1}", <i>__1, Time.realtimeSinceStartup);
$current = new WaitForSeconds(2f);
if (!$disposing)
{
$PC = 1;
}
break;
}
$PC = -1;
goto default;
}
return true;
}
[DebuggerHidden]
public void Dispose()
{
$disposing = true;
$PC = -1;
}
[DebuggerHidden]
public void Reset()
{
throw new NotSupportedException();
}
}
这个<Init1>c_Iterator1的类,实现了IEnumerator, IDisposable, IEnumerator<object>接口, 编译器根据Init1函数体, 写了一个可以保存当前迭代状态(迭代到了第几个元素)的类。
这个类是编译器自动生成的, 而我们自己写的函数Init1函数时, 则变成了这样:
private IEnumerator Init1()
{
return new <Init1>c__Iterator1();
}
直接返回一个Init1的迭代器类,所以StartCoroutine(Init1()),就是将一个 <Init1>c__Iterator1()的迭代器实例传给StartCoroutine, 之后的处理就是Unity不公开的部分了, 按照文档信息可以推论,这些StartCorountine函数返回的Coroutine应该都被Unity GameObject管理起来, 在每一帧的不同位置, 找出那些Yeild命令得到满足的Coroutine, 进行它的下一次迭代。
异常处理
try{}catch(){}语句中是不能包含yeild指令(除了yield break)的,编译没法通过:
Cannot yield a value in the body of a try block with a catch clause
如果协程里yield语句之后的操作发生了异常, 这个协程将不会返回, 如果在父协程里有这样一种顺序:
IEnumerator Init()
{
Debug.Log("Pre Start: " + Time.realtimeSinceStartup);
yield return StartCoroutine(init2);
Debug.Log("All End: " + Time.realtimeSinceStartup);
//do something
}
IEnumerator Init2()
{
for (int i = 0; i < 5; i++)
{
yield return new WaitForSeconds(2);
//gameobject根本没有camera,这里会有一个MissingComponentException异常
gameObject.GetComponent<Camera>().clearFlags = CameraClearFlags.SolidColor;
Debug.LogFormat("init2 enter: {0}, {1}", i, Time.realtimeSinceStartup);
}
}
这种情况下, Init2函数种,需要返回2秒后,修改camera的属性, 但是因为没有camera, 抛出异常了, 此时, Init函数无法从Init2种返回,它后面的语句将不再执行。要排除这种情况,有一个办法就是在可能出异常的地方,单独catch下:
IEnumerator Init2()
{
for (int i = 0; i < 5; i++)
{
yield return new WaitForSeconds(2);
try
{
gameObject.GetComponent<Camera>().clearFlags = CameraClearFlags.SolidColor;
Debug.LogFormat("init2 Try: {0}, {1}", i, Time.realtimeSinceStartup);
}
catch (Exception e)
{
Debug.LogFormat("init2 exception: {0}, {1}, {2}", i, Time.realtimeSinceStartup, e.Message);
yield break;
}
}
这样子, 就能顺利返回Init, 不影响之后的功能了。但这种方式写出来的协程会很冗长, 到处都是try ...catch。
解决办法,就是在unity和协程之间加一个中间层。
IEnumerator InitWrapper(IEnumerator enumerator)
{
Debug.Log("Pre Start: " + Time.realtimeSinceStartup);
while (true)
{
try
{
if (enumerator == null || !enumerator.MoveNext())
{
Debug.Log("All End: " + Time.realtimeSinceStartup);
yield break;
}
}
catch (Exception e)
{
Debug.Log("All End: with exception" + Time.realtimeSinceStartup);
yield break;
}
yield return enumerator.Current;
}
}
这个中间协程InitWrapper接管了自协程的迭代,这样就能catch住除了yield return enumerator.Current之外的其他异常(这个语句很简单, 基本不会发生异常), 它的功能也很好理解, 其实就是一个迭代器, 这个迭代器的唯一功能就是返回它管理的子协程的每一次迭代。
通过这个方式, 就可以保证协程能正常返回, 不会被卡住。