轉載請標明出處:http://www.cnblogs.com/zblade/
一、序言
在unity的游戲開發中,對於異步操作,有一個避免不了的操作: 協程,以前一直理解的懵懵懂懂,最近認真充電了一下,通過前輩的文章大體理解了一下,在這兒拋磚引玉寫一些個人理解。當然首先給出幾篇寫的非常精彩優秀的文章,最好認真拜讀一下:
Unity3d中協程的原理,你要的yield return new xxx的真正理解之道
好了,接下來就從一個小白的視角開始理解協程。
二、常見使用協程的示例
經常,我們會利用monobehaviour的startcoroutine來開啟一個協程,這是我們在使用unity中最常見的直觀理解。在這個協程中執行一些異步操作,比如下載文件,加載文件等,在完成這些操作后,執行我們的回調。 舉例說明:
public static void Download(System.Action finishCB)
{
string url = "https: xxxx";
StartCoroutine(DownloadFile(url));
}
private static IEnumerator DownloadFile(string url)
{
UnityWebRequest request = UnityWebRequest.Get(url);
request.timeout = 10;
yield return request.SendWebRequest();
if(request.error != null)
{
Debug.LogErrorFormat("加載出錯: {0}, url is: {1}", request.error, url);
request.Dispose();
yield break;
}
if(request.isDone)
{
string path = "xxxxx";
File.WriteAllBytes(path, request.downloadHandler.data);
request.Dispose();
yiled break;
}
}
這個例子中,用到了幾個關鍵詞: IEnumerator/yield return xxx/ yield break/StartCoroutine, 那么我們從這幾個關鍵詞入手,去理解這樣的一個下載操作具體實現。
1、關鍵詞 IEnumerator
這個關鍵詞不是在Unity中特有,unity也是來自c#,所以找一個c#的例子來理解比較合適。首先看看IEnumerator的定義:
public interface IEnumerator
{
bool MoveNext();
void Reset();
Object Current{get;}
}
從定義可以理解,一個迭代器,三個基本的操作:Current/MoveNext/Reset, 這兒簡單說一下其操作的過程。在常見的集合中,我們使用foreach這樣的枚舉操作的時候,最開始,枚舉數被定為在集合的第一個元素前面,Reset操作就是將枚舉數返回到此位置。
迭代器在執行迭代的時候,首先會執行一個 MoveNext, 如果返回true,說明下一個位置有對象,然后此時將Current設置為下一個對象,這時候的Current就指向了下一個對象。當然c#是如何將這個IEnumrator編譯成一個對象示例來執行,下面會講解到。
2、關鍵詞 Yield
c#中的yield關鍵詞,后面有兩種基本的表達式:
yield return <expresion> yiled break
yield break就是跳出協程的操作,一般用在報錯或者需要退出協程的地方。
yield return是用的比較多的表達式,具體的expresion可以以下幾個常見的示例:
WWW : 常見的web操作,在每幀末調用,會檢查isDone/isError,如果true,則 call MoveNext WaitForSeconds: 檢測間隔時間是否到了,返回true, 則call MoveNext null: 直接 call MoveNext WaitForEndOfFrame: 在渲染之后調用, call MoveNext
好了,有了對幾個關鍵詞的理解,接下來我們看看c#編譯器是如何把我們寫的協程調用編譯生成的。
三、c#對協程調用的編譯結果
這兒沒有把上面的例子編譯生成,就借用一下前面文章中的例子 :b
class Test
{
static IEnumerator GetCounter()
{
for(int count = 0; count < 10; count++)
{
yiled return count;
}
}
}
其編譯器生成的c++結果:
internal class Test
{
// GetCounter獲得結果就是返回一個實例對象
private static IEnumerator GetCounter()
{
return new <GetCounter>d__0(0);
}
// Nested type automatically created by the compiler to implement the iterator
[CompilerGenerated]
private sealed class <GetCounter>d__0 : IEnumerator<object>, IEnumerator, IDisposable
{
// Fields: there'll always be a "state" and "current", but the "count"
// comes from the local variable in our iterator block.
private int <>1__state;
private object <>2__current;
public int <count>5__1;
[DebuggerHidden]
public <GetCounter>d__0(int <>1__state)
{
//初始狀態設置
this.<>1__state = <>1__state;
}
// Almost all of the real work happens here
//類似於一個狀態機,通過這個狀態的切換,可以將整個迭代器執行過程中的堆棧等環境信息共享和保存
private bool MoveNext()
{
switch (this.<>1__state)
{
case 0:
this.<>1__state = -1;
this.<count>5__1 = 0;
while (this.<count>5__1 < 10) //這里針對循環處理
{
this.<>2__current = this.<count>5__1;
this.<>1__state = 1;
return true;
Label_004B:
this.<>1__state = -1;
this.<count>5__1++;
}
break;
case 1:
goto Label_004B;
}
return false;
}
[DebuggerHidden]
void IEnumerator.Reset()
{
throw new NotSupportedException();
}
void IDisposable.Dispose()
{
}
object IEnumerator<object>.Current
{
[DebuggerHidden]
get
{
return this.<>2__current;
}
}
object IEnumerator.Current
{
[DebuggerHidden]
get
{
return this.<>2__current;
}
}
}
}
代碼比較直觀,相關的注釋也寫了一點,所以我們在執行開啟一個協程的時候,其本質就是返回一個迭代器的實例,然后在主線程中,每次update的時候,都會更新這個實例,判斷其是否執行MoveNext的操作,如果可以執行(比如文件下載完成),則執行一次MoveNext,將下一個對象賦值給Current(MoveNext需要返回為true, 如果為false表明迭代執行完成了)。
通過這兒,可以得到一個結論,協程並不是異步的,其本質還是在Unity的主線程中執行,每次update的時候都會觸發是否執行MoveNext。
四、協程的衍生使用
既然IEnumerator可以這樣用,那我們其實可以只使用MoveNext和Current,就可以寫一個簡易的測試協程的例子,Ok,來寫一個簡易的例子,來自leader的代碼,偷懶就復用了 :D
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Profiling;
public class QuotaCoroutine : MonoBehaviour
{
// 每幀的額度時間,全局共享
static float frameQuotaSec = 0.001f;
static LinkedList<IEnumerator> s_tasks = new LinkedList<IEnumerator>();
// Use this for initialization
void Start()
{
StartQuotaCoroutine(Task(1, 100));
}
// Update is called once per frame
void Update()
{
ScheduleTask();
}
void StartQuotaCoroutine(IEnumerator task)
{
s_tasks.AddLast(task);
}
static void ScheduleTask()
{
float timeStart = Time.realtimeSinceStartup;
while (s_tasks.Count > 0)
{
var t = s_tasks.First.Value;
bool taskFinish = false;
while (Time.realtimeSinceStartup - timeStart < frameQuotaSec)
{
// 執行任務的一步, 后續沒步驟就是任務完成
Profiler.BeginSample(string.Format("QuotaTaskStep, f:{0}", Time.frameCount));
taskFinish = !t.MoveNext();
Profiler.EndSample();
if (taskFinish)
{
s_tasks.RemoveFirst();
break;
}
}
// 任務沒結束執行到這里就是沒時間額度了
if (!taskFinish)
return;
}
}
IEnumerator Task(int taskId, int stepCount)
{
int i = 0;
while (i < stepCount)
{
Debug.LogFormat("{0}.{1}, frame:{2}", taskId, i, Time.frameCount);
i++;
yield return null;
}
}
}
說一下思路: 在開始的時候,構建一個IEnuerator實例塞入鏈表中,然后再后續的每幀update的時候,取出這個實例,執行一次MoveNext,一直到都執行完后,移除這個實例,這樣就不用顯示的調用StartCoroutine,也可以類似的觸發執行MoveNext :D
看運行結果:
可行。OK,關於unity的協程就寫到這兒了,接下來將一下xlua中對於協程的實現。
五、Lua中的協程
Lua中的協程和unity協程的區別,最大的就是其不是搶占式的執行,也就是說不會被主動執行類似MoveNext這樣的操作,而是需要我們去主動激發執行,就像上一個例子一樣,自己去tick這樣的操作。
Lua中協程關鍵的三個API:
coroutine.create()/wrap: 構建一個協程, wrap構建結果為函數,create為thread類型對象
coroutine.resume(): 執行一次類似MoveNext的操作
coroutine.yield(): 將協程掛起
比較簡易,可以寫也給例子測試一下:
local func = function(a, b)
for i= 1, 5 do
print(i, a, b)
end
end
local func1 = function(a, b)
for i = 1, 5 do
print(i, a, b)
coroutine.yield()
end
end
co = coroutine.create(func)
coroutine.resume(co, 1, 2)
--此時會輸出 1 ,1, 2/ 2,1,2/ 3, 1,2/4,1,2/5,1,2
co1 = coroutine.create(func1)
coroutine.resume(co1, 1, 2)
--此時會輸出 1, 1,2 然后掛起
coroutine.resume(co1, 3, 4)
--此時將上次掛起的協程恢復執行一次,輸出: 2, 1, 2 所以新傳入的參數3,4是無效的
我們來看看xlua開源出來的util中對協程的使用示例又是怎么結合lua的協程,在lua端構建也給協程,讓c#端也可以獲取這個實例,從而添加到unity端的主線程中去觸發update。
看一下調用的API:
local util = require 'xlua.util' local gameobject = CS.UnityEngine.GameObject('Coroutine_Runner') CS.UnityEngine.Object.DontDestroyOnLoad(gameobject) local cs_coroutine_runner = gameobject:AddComponent(typeof(CS.Coroutine_Runner)) return { start = function(...) return cs_coroutine_runner:StartCoroutine(util.cs_generator(...)) end; stop = function(coroutine) cs_coroutine_runner:StopCoroutine(coroutine) end }
start操作,本質就是將function包一層,調用util.csgenerator,進一步看看util中對cs_generator的實現:
local move_end = {} local generator_mt = { __index = { MoveNext = function(self) self.Current = self.co() if self.Current == move_end then self.Current = nil return false
else
return true end end; Reset = function(self) self.co = coroutine.wrap(self.w_func) end } } local function cs_generator(func, ...) local params = {...} local generator = setmetatable({ w_func = function() func(unpack(params)) return move_end end }, generator_mt) generator:Reset() return generator end
代碼很短,不過思路很清晰,首先構建一個table, 其中的key對應一個function,然后修改去元表的_index方法,其中包含了MoveNext函數的實現,也包含了Reset函數的實現,不過這兒的Reset和IEnumerator的不一樣,這兒是調用coroutine.wrap來生成一個協程。這樣c#端獲取到這個generator的handleID后,后面每幀update回來都會執行一次MoveNext,如果都執行完了,這時候會return move_end,表明協程都執行完了,返回false給c#端清空該協程的handleID.