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之外的其他異常(這個語句很簡單, 基本不會發生異常), 它的功能也很好了解, 其實就是一個疊代器, 這個疊代器的唯一功能就是傳回它管理的子協程的每一次疊代。
通過這個方式, 就可以保證協程能正常傳回, 不會被卡住。