http://gad.qq.com/article/detail/695
app
使用Unity 3D引擎的同窗,對於Coroutine(協程)的使用確定也是很是熟悉的了。然而Coroutine背後的技術以及具體的實現方式、運行流程如何,恐怕並非那麼容易說得清楚。xss
本文嘗試經過分析Unity 3.5.7版本的源代碼,來釐清這一關鍵技術細節。(因爲Unity 3.5.7的源代碼,能拿到的版本沒法編譯、運行,只能直接經過源代碼閱讀的方式來進行靜態的梳理)。編輯器
C#層面的Coroutine:背後的技術svg
IEnumerator接口與yield語句函數
IEnumerator自己是一個forward iterator,定義以下:oop
public interface IEnumeratorn
{
object Current { get }
bool MoveNext();
void Reset();
}源碼分析
最多見的使用方式,是在foreach中使用,foreach又可展開爲(簡化版本):性能
while (i.MoveNext()) // i implements IEnumeratorspa
{3d
object o = i.Current;
// do stuff with o...
}
.Net爲了簡化IEnumerator的實現,引入了yield語句:
http://msdn.microsoft.com/en-us/library/9k7k7cf0.aspx
簡單來講,yield return用於在每次迭代時返回一個元素,這個元素能夠經過IEnumerator.Current屬性訪問;同時,yield語句會保留迭代器的當前狀態,當下次迭代時,保證狀態連續性。
關於IEnumerator/yield的細節,推薦參考《深刻理解C#》一書的第六章:「實現迭代器的捷徑」
http://book.douban.com/subject/7055340/
Unity中的Coroutine
以上都是理論基礎。因爲yield語句實際上用到了比較高級的編譯器技術,只是瞭解理論老是有些隔靴搔癢的感受。那麼,讓咱們來看一下Unity中是如何「實踐」的。
這裏以一個簡單的Coroutine函數爲例:
IEnumerator Fade()
{
for (float f = 1f; f >= 0; f -= 0.1f)
{
Color c = renderer.material.color;
c.a = f;
renderer.material.color = c;
yield return new WaitForSeconds(0.1f);
}
}
在Reflector中,咱們能夠觀察一下實際上生成的代碼:
注意這個<Fade>c__Iterator15 ,就是編譯器幫咱們生成的輔助類,實現了IEnumerator接口:
咱們還能夠進一步展開MoveNext()方法來查看:
因而能夠發現,咱們寫在Fade()中的代碼,都出如今了MoveNext()中,而編譯器將其展開放在了一個有限狀態機中;而yield return語句,實際上直接賦值給了current:
至此,Coroutine在C#層面所涉及的技術,咱們已經有了一個大致的瞭解。
C++運行時分析
咱們知道,Unity底層引擎是採用C++編寫,而後經過一箇中間層將全部功能封裝在UnityEngine.dll中(UnityEditor.dll對應編輯器功能),供上層C#(以及Unitynoxss、Boo)調用。
咱們能夠在Reflector中查看這個dll,以下圖:
在Unity的源碼中,引擎的運行時代碼所有位於根目錄的「Runtime」目錄下。
這部分封裝層,在Unity源代碼中對應的是Runtime¥Export目錄下的內容。Unity所使用的應該是自家的Wrapper技術,具體未深究。好在查看其中的原始文件依然能讓咱們一探究竟。
好比Coroutine相關入口,所對應的Wrapper文件就是Runtime¥Export¥UnityEngineMonobBehaviour.txt文件。以此爲入口,咱們能夠開始一步步分析Coroutine的相關實現了。
在瀏覽了相關代碼時,我整理出了一個大體的調用關係圖,以下(svg版本見附件):
須要說明的是,圖中已經對於細節作了大量簡化,只關注了StartCoroutine的主要流程,而忽略了錯誤處理、Coroutine銷燬、參數對象生命週期管理等細節。
如下爲詳細的流程分析:
1, C#層調用StartCoroutine方法,將IEnumerator對象(或者是用於建立IEnumerator對象的方法名字符串)傳入C++層,建立出一個對應的Coroutine對象,以後調用這個Coroutine對象的Run()方法;
2, 在Coroutine.Run()中,首先經過mono的反射功能,找到IEnumerator上的MoveNext()、get_Current()兩個方法,而後調用一次MoveNext();若是MoveNext()返回false,表示Coroutine執行結束,進入清理流程(上圖中忽略);若是返回true,表示Coroutine執行到了一句yield return處;這時就須要調用get_Current()取出yield return返回的對象(monoWait),再根據monoWait的具體類型(null、WaitForSeconds、WaitForFixedUpdate等),將Coroutine對象保存到DelayedCallManager的callback列表中;至此,Coroutine在當前幀的執行即結束;
3, 以後遊戲運行過程當中,遊戲主循環的PlayerLoop()方法會在每幀的不一樣時間點以不一樣的modeMask調用DelayedCallManager.Update方法(注:PlayerLoop中的具體過程以及調用Coroutine時間點,能夠參考:http://docs.unity3d.com/uploads/Main/monobehaviour_flowchart.svg);Update方法中會遍歷callback列表中的Coroutine對象;若是某個Coroutine對象的monoWait的執行條件知足,則將其從callback列表中取出,執行這個Coroutine對象的Run()方法,回到2的執行流程中。
至此,Coroutine的總體流程已經分析完畢,沒有什麼疑惑之處了。
後記
在掌握了Coroutine機制的實現細節以後,對於一些更深層次的問題也天然會有更進一步的認識。
好比StartCoroutine提供的兩個版本:
public Coroutine StartCoroutine(IEnumerator routine);
public Coroutine StartCoroutine(string methodName);
雖然功能上一致,但在實現細節上其實有比較大的差別。從運行時效率來看,字符串的版本在運行時要用mono的反射作更多參數檢查、函數查詢工做(調用圖中藍色線條部分),帶來性能損失。
實際上在老版本的Unity中,字符串版本的StartCoroutine最大的做用,是能夠調用StopCoroutine來中止對應的Coroutine。而在4.5.0版本中,Unity終於加上了
public void StopCoroutine(IEnumerator routine);
這一接口,因而使用IEnumerator啓動的Coroutine,也能夠被手動終止了。
結合咱們分析源碼得出的結論,無疑應當儘可能避免使用字符串版本StartCoroutine。