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。