Unity开发不可避免的要用到协程(Coroutine),协程同步代码作异步任务的特性使程序员摆脱了曾经异步操做加回调的编码方式,使代码逻辑更加连贯易读。然而在惊讶于协程的好用与神奇的同时,由于不清楚协程背后的实现原理,因此老是感受没法彻底掌握协程。好比:html
MonoBehaviour.StartCoroutine
接收的参数为何是IEnumerator
,IEnumerator
和协程有什么关系?IEnumerator
,为何函数内yield return
的又是不一样类型的返回值?yield
是什么,常见的yield return
,yield break
是什么意思,又有什么区别?yield return
就可使代码“停”在那里,达到某种条件后又能够从“停住”的地方继续执行?yield return new WaitForSeconds(3)
,yield return webRequest.SendWebRequest()
,为何能够实现等待指定时间或是等待请求完成再接着执行后面的代码?若是你和我同样也有上面的疑问,不妨阅读下本文,相信必定能够解答你的疑惑。c++
根据微软官方文档的描述,IEnumerator是全部非泛型枚举器的基接口。换而言之就是IEnumerator定义了一种适用于任意集合的迭代方式。任意一个集合只要实现本身的IEnumerator,它的使用者就能够经过IEnumerator迭代集合中的元素,而不用针对不一样的集合采用不一样的迭代方式。git
IEnumerator的定义以下所示程序员
public interface IEnumerator { object Current { get; } bool MoveNext(); void Reset(); }
IEnumerator接口由一个属性和两个方法组成github
好比咱们常常会使用的foreach关键字遍历集合,其实foreach
只是C#提供的语法糖而已web
foreach (var item in collection) { Console.WriteLine(item.ToString()); }
本质上foreach
循环也是采用IEnumerator来遍历集合的。在编译时编译器会将上面的foreach
循环转换为相似于下面的代码c#
{ var enumerator = collection.GetEnumerator(); try { while (enumerator.MoveNext()) // 判断是否成功推动到下一个元素(可理解为集合中是否还有可供迭代的元素) { var item = enumerator.Current; Console.WriteLine(item.ToString()); } } finally { // dispose of enumerator. } }
yield是C#的关键字,其实就是快速定义迭代器的语法糖。只要是yield
出如今其中的方法就会被编译器自动编译成一个迭代器,对于这样的函数能够称之为迭代器函数。迭代器函数的返回值就是自动生成的迭代器类的一个对象api
试试想象若是没有yield
关键字,咱们每定义一个迭代器,就要建立一个类,实现IEnumerator
接口,接口包含的属性与方法都要正确的实现,是否是很麻烦?而利用yield
关键字,只须要下面简单的几行代码,就能够快速定义一个迭代器。诸如迭代器类的建立,IEnumerator
接口的实现工做编译器统统帮你作了浏览器
// 由迭代器函数定义的迭代器 IEnumerator Test() { yield return 1; Debug.Log("Surprise"); yield return 3; yield break; yield return 4; }
yield return
语句能够返回一个值,表示迭代获得的当前元素yield break
语句能够用来终止迭代,表示当前没有可被迭代的元素了以下所示,能够经过上面代码定义的迭代器遍历元素网络
IEnumerator enumerator = Test(); // 直接调用迭代器函数不会执行方法的主体,而是返回迭代器对象 bool ret = enumerator.MoveNext(); Debug.Log(ret + " " + enumerator.Current); // (1)打印:True 1 ret = enumerator.MoveNext(); // (2)打印:Surprise Debug.Log(ret + " " + enumerator.Current); // (3)打印:True 3 ret = enumerator.MoveNext(); Debug.Log(ret + " " + enumerator.Current); // (4)打印:False 3
(1)(3)(4)处的打印都没有什么问题,(1)(3)正确打印出了返回的值,(4)是由于迭代被yield break
终止了,因此MoveNext
返回了false
重点关注(2)打印的位置,是在第二次调用MoveNext
函数以后触发的,也就是说若是不调用第二次的MoveNext
,(2)打印将不会被触发,也意味着Debug.Log("Surprise")
这句代码不会被执行。表现上来看yield return 1
好像把代码“停住”了,当再次调用MoveNext
方法后,代码又从“停住”的地方继续执行了
想要搞清楚代码“停住”又原位恢复的原理,就要去IL中找答案了。可是编译生成的IL是相似于汇编语言的中间语言,比较底层且晦涩难懂。因此我利用了Unity的IL2CPP,它会将C#编译生成的IL再转换成C++语言。能够经过C++代码的实现来曲线研究yield return
的实现原理
好比下面的C#类,为了便于定位函数内的变量,因此变量名就起的复杂点
public class Test { public IEnumerator GetSingleDigitNumbers() { int m_tag_index = 0; int m_tag_value = 0; while (m_tag_index < 10) { m_tag_value += 456; yield return m_tag_index++; } } }
生成的类在Test.cpp文件中,因为文件比较长,因此只截取部分重要的片断(有删减,完整的文件能够查看这里)
// Test/<GetSingleDigitNumbers>d__0 struct U3CGetSingleDigitNumbersU3Ed__0_t9371C0E193B6B7701AD95F88620C6D6C93705F1A : public RuntimeObject { public: // System.Int32 Test/<GetSingleDigitNumbers>d__0::<>1__state int32_t ___U3CU3E1__state_0; // System.Object Test/<GetSingleDigitNumbers>d__0::<>2__current RuntimeObject * ___U3CU3E2__current_1; // Test Test/<GetSingleDigitNumbers>d__0::<>4__this Test_tD0155F04059CC04891C1AAC25562964CCC2712E3 * ___U3CU3E4__this_2; // System.Int32 Test/<GetSingleDigitNumbers>d__0::<m_tag_index>5__1 int32_t ___U3Cm_tag_indexU3E5__1_3; // System.Int32 Test/<GetSingleDigitNumbers>d__0::<m_tag_value>5__2 int32_t ___U3Cm_tag_valueU3E5__2_4; public: inline int32_t get_U3CU3E1__state_0() const { return ___U3CU3E1__state_0; } inline void set_U3CU3E1__state_0(int32_t value) { ___U3CU3E1__state_0 = value; } inline RuntimeObject * get_U3CU3E2__current_1() const { return ___U3CU3E2__current_1; } inline void set_U3CU3E2__current_1(RuntimeObject * value) { ___U3CU3E2__current_1 = value; Il2CppCodeGenWriteBarrier((void**)(&___U3CU3E2__current_1), (void*)value); } inline int32_t get_U3Cm_tag_indexU3E5__1_3() const { return ___U3Cm_tag_indexU3E5__1_3; } inline void set_U3Cm_tag_indexU3E5__1_3(int32_t value) { ___U3Cm_tag_indexU3E5__1_3 = value; } inline int32_t get_U3Cm_tag_valueU3E5__2_4() const { return ___U3Cm_tag_valueU3E5__2_4; } inline void set_U3Cm_tag_valueU3E5__2_4(int32_t value) { ___U3Cm_tag_valueU3E5__2_4 = value; } };
能够看到GetSingleDigitNumbers
函数确实被定义成了一个类U3CGetSingleDigitNumbersU3Ed__0_t9371C0E193B6B7701AD95F88620C6D6C93705F1A
,而局部变量m_tag_index
和m_tag_value
都分别被定义成了这个类的成员变量___U3Cm_tag_indexU3E5__1_3
和___U3Cm_tag_valueU3E5__2_4
,而且为它们生成了对应的get和set方法。___U3CU3E2__current_1
成员变量对应IEnumerator
的Current
属性。这里再关注下额外生成的___U3CU3E1__state_0
成员变量,能够理解为一个状态机,经过它表示的不一样状态值,决定了整个函数逻辑应该如何执行,后面会看到它是如何起做用的。
// System.Boolean Test/<GetSingleDigitNumbers>d__0::MoveNext() IL2CPP_EXTERN_C IL2CPP_METHOD_ATTR bool U3CGetSingleDigitNumbersU3Ed__0_MoveNext_mED8994A78E174FF0A8BE28DF873D247A3F648CFB (U3CGetSingleDigitNumbersU3Ed__0_t9371C0E193B6B7701AD95F88620C6D6C93705F1A * __this, const RuntimeMethod* method) { static bool s_Il2CppMethodInitialized; if (!s_Il2CppMethodInitialized) { il2cpp_codegen_initialize_method (U3CGetSingleDigitNumbersU3Ed__0_MoveNext_mED8994A78E174FF0A8BE28DF873D247A3F648CFB_MetadataUsageId); s_Il2CppMethodInitialized = true; } int32_t V_0 = 0; int32_t V_1 = 0; bool V_2 = false; { int32_t L_0 = __this->get_U3CU3E1__state_0(); V_0 = L_0; int32_t L_1 = V_0; if (!L_1) { goto IL_0012; } } { goto IL_000c; } IL_000c: { int32_t L_2 = V_0; if ((((int32_t)L_2) == ((int32_t)1))) { goto IL_0014; } } { goto IL_0016; } IL_0012: { goto IL_0018; } IL_0014: { goto IL_0068; } IL_0016: { return (bool)0; } IL_0018: { __this->set_U3CU3E1__state_0((-1)); // int m_tag_index = 0; __this->set_U3Cm_tag_indexU3E5__1_3(0); // int m_tag_value = 0; __this->set_U3Cm_tag_valueU3E5__2_4(0); goto IL_0070; } IL_0030: { // m_tag_value += 456; int32_t L_3 = __this->get_U3Cm_tag_valueU3E5__2_4(); __this->set_U3Cm_tag_valueU3E5__2_4(((int32_t)il2cpp_codegen_add((int32_t)L_3, (int32_t)((int32_t)456)))); // yield return m_tag_index++; int32_t L_4 = __this->get_U3Cm_tag_indexU3E5__1_3(); V_1 = L_4; int32_t L_5 = V_1; __this->set_U3Cm_tag_indexU3E5__1_3(((int32_t)il2cpp_codegen_add((int32_t)L_5, (int32_t)1))); int32_t L_6 = V_1; int32_t L_7 = L_6; RuntimeObject * L_8 = Box(Int32_t585191389E07734F19F3156FF88FB3EF4800D102_il2cpp_TypeInfo_var, &L_7); __this->set_U3CU3E2__current_1(L_8); __this->set_U3CU3E1__state_0(1); return (bool)1; } IL_0068: { __this->set_U3CU3E1__state_0((-1)); } IL_0070: { // while (m_tag_index < 10) int32_t L_9 = __this->get_U3Cm_tag_indexU3E5__1_3(); V_2 = (bool)((((int32_t)L_9) < ((int32_t)((int32_t)10)))? 1 : 0); bool L_10 = V_2; if (L_10) { goto IL_0030; } } { // } return (bool)0; } }
而U3CGetSingleDigitNumbersU3Ed__0_MoveNext_mED8994A78E174FF0A8BE28DF873D247A3F648CFB
成员方法对应了IEnumerator
的MoveText
方法。它的实现利用了goto语句,而这个方法正是代码“停住”与恢复的关键所在
咱们一步步来看,按照c#代码的逻辑,第一次调用moveNext
函数时,应该执行如下代码
int m_tag_index = 0; int m_tag_value = 0; if (m_tag_index < 10) { m_tag_value += 456; return m_tag_index++; }
对应执行的c++代码以下所示。执行完毕IL_0030完毕后,将返回true,表示还有元素。此时的state为1
// 初始时,___U3CU3E1__state_0值为0 goto IL_0012; goto IL_0018; // IL_0018内部初始化m_tag_index和m_tag_value为0. 同时设置___U3CU3E1__state_0值为-1 goto IL_0070; // 判断m_tag_index是否小于10 goto IL_0030; // IL_0030内部将m_tag_index值加1,并将m_tag_index的值设置为current值,并将___U3CU3E1__state_0值设置为1
第二次调用moveNext
函数,对应C#代码为
if (m_tag_index < 10) { m_tag_value += 456; return m_tag_index++; }
对应的c++代码为
// 此时___U3CU3E1__state_0值为1,根据判断进入IL_000c goto IL_000c; goto IL_0014; goto IL_0068; // 设置___U3CU3E1__state_0为-1 IL_0070 // 判断m_tag_index是否小于10 goto IL_0030; // 返回1,表示true,还有可迭代元素
当第11次调用moveNext
函数时,m_tag_index
的值已是10,此时函数应该结束。返回值应该是false,表示没有再能返回的元素了。
因此对应的C++代码为
// ___U3CU3E1__state_0值是1 goto IL_000c; goto IL_0014; goto IL_0068 IL_0070 // 判断m_tag_index是不小于10的,因此不会进入IL_0030 { // } return (bool)0; }
到这里,我想代码“停住”与恢复的神秘面纱终于被揭开了。总结下来就是,以能“停住”的地方为分界线,编译器会为不一样分区的语句按照功能逻辑生成一个个对应的代码块。yield
语句就是这条分界线,想要代码“停住”,就不执行后面语句对应的代码块,想要代码恢复,就接着执行后面语句对应的代码块。而调度上下文的保存,是经过将须要保存的变量都定义成成员变量来实现的。
如今咱们能够讨论下yield return
与协程的关系了,或者说IEnumerator与协程的关系
协程是一种比线程更轻量级的存在,协程可彻底由用户程序控制调度。协程能够经过yield方式进行调度转移执行权,调度时要可以保存上下文,在调度回来的时候要可以恢复。这是否是和上面“停住”代码而后又原位恢复的执行效果很像?没错,Unity实现协程的原理,就是经过yield return
生成的IEnumerator
再配合控制什么时候触发MoveNext
来实现了执行权的调度
具体而言,Unity每经过MonoBehaviour.StartCoroutine
启动一个协程,就会得到一个IEnumerator
(StartCoroutine
的参数就是IEnumerator
,参数是方法名的重载版本也会经过反射拿到该方法对应的IEnumerator
)。并在它的游戏循环中,根据条件判断是否要执行MoveNext
方法。而这个条件就是根据IEnumerator
的Current
属性得到的,即yield return
返回的值。
在启动一个协程时,Unity会先调用获得的IEnumerator
的MoveNext
一次,以拿到IEnumerator
的Current
值。因此每启动一个协程,协程函数会当即执行到第一个yield return
处而后“停住”。
对于不一样的Current
类型(通常是YieldInstruction
的子类),Unity已作好了一些默认处理,好比:
若是Current
是null
,就至关于什么也不作。在下一次游戏循环中,就会调用MoveNext
。因此yield return null
就起到了等待一帧的做用
若是Current
是WaitForSeconds
类型,Unity会获取它的等待时间,每次游戏循环中都会判断时间是否到了,只有时间到了才会调用MoveNext
。因此yield return WaitForSeconds
就起到了等待指定时间的做用
若是Current
是UnityWebRequestAsyncOperation
类型,它是AsyncOperation
的子类,而AsyncOperation
有isDone
属性,表示操做是否完成,只有isDone
为true时,Unity才会调用MoveNext
。对于UnityWebRequestAsyncOperation
而言,只有请求完成了,才会将isDone
属性设置为true。
也所以咱们才可使用下面的同步代码,完成原本是异步的网络请求操做。
using(UnityWebRequest webRequest = UnityWebRequest.Get("https://www.cnblogs.com/iwiniwin/p/13705456.html")) { yield return webRequest.SendWebRequest(); if(webRequest.isNetworkError) { Debug.Log("Error " + webRequest.error); } else { Debug.Log("Received " + webRequest.downloadHandler.text); } }
Unity的协程是和MonoBehavior进行了绑定的,只能经过MonoBehavior.StartCoroutine
开启协程,而在开发中,有些不是继承MonoBehavior的类就没法使用协程了,在这种状况下咱们能够本身封装一套协程。在搞清楚Unity协程的实现原理后,想必实现本身的协程也不是难事了,感兴趣的同窗赶快行动起来吧。
这里有一份Remote File Explorer内已经封装好的实现,被用于制做Editor工具时没法使用MonoBehavior又想使用协程的状况下。Remote File Explorer是一个跨平台的远程文件浏览器,使用户经过Unity Editor就能操做应用所运行平台上的目录文件,其内部消息通信部分大量使用了协程,是了解协程同步代码实现异步任务特性的不错的例子
固然Unity Editor下使用协程,Unity也提供了相关的包,能够参考Editor Coroutines