开启一个协程发生了什么

分析如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class TestScript : MonoBehaviour
{
private void Start()
{
StartCoroutine(ShowLog());
}

IEnumerator ShowLog()
{
for(int i = 0; i < 100; i++)
{
Debug.Log(i);
yield return new WaitForSeconds(1f);
}
}
}

先描述一下里面用到的方法和类:

StartCoroutine

StartCoroutine是 Monobehavior类的函数,有3个重载函数

public Coroutine StartCoroutine(string methodName);
public Coroutine StartCoroutine(IEnumerator routine);
public Coroutine StartCoroutine(string methodName, [DefaultValue(“null”)] object value);

StartCoroutine的第一个和第三个methodName ,都是一个返回类型是IEnumerator的方法。

所以入参全是IEnumerator迭代器方法。返回类型全是Coroutine类。

Coroutine

协同程序。继承自YieldInstruction类。

StartCoroutine函数 返回 Coroutine。协同程序是一个可以暂停执行 (yield) 的函数,直到给定的 YieldInstruction 完成。

YieldInstruction

yield return后面可以是值,也可以是一个类型为继承自YieldInstruction的类

如果yield return的是YieldInstruction的派生类,Unity就会将其理解为“持续等待”。比如WaitForEndOfFrame、WaitForFixedUpdate、WaitForSeconds、WWW、Coroutine(StartCoroutine的返回值),它们都是。

yield return

yield return后面可以跟的表达式:

​ 所有非YieldInstruction派生类(包括null):协程将会在下一帧恢复,继续后续代码。

​ WaitForEndOfFrame:协程将会在这一帧结束之后(所有渲染、GUI)恢复,继续后续代码。

​ WaitForFixedUpdate:所有物理引擎计算完成之后恢复,继续后续代码。

​ WaitForSeconds:等待x秒后(以Unity内的计时系统为基准)恢复,继续后续代码。

​ WWW:等待一个web request结束后恢复,继续后续代码。

​ Coroutine 其他协程(协程嵌套):等子协程Coroutine执行完后恢复,继续后续代码。如果子协程内有yield中断,那父协程会一直暂停,直到子协程运行完毕。

CustomYieldInstruction

想实现自定义和YieldInstruction一样,拥有“持续等待”逻辑的协程,就用这个。继承自IEnumerator类。

重写keepWaiting函数即可。要使协同程序保持暂停,则返回true;要使协同程序继续执行, 则返回false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
using System.Collections;
using UnityEngine;

public class ExampleScript : MonoBehaviour
{
void Update()
{
if (Input.GetMouseButtonUp(0))
{
Debug.Log("Left mouse button up");
StartCoroutine(waitForMouseDown());
}
}

public IEnumerator waitForMouseDown()
{
yield return new WaitForMouseDown();
Debug.Log("Right mouse button pressed");
}
}

public class WaitForMouseDown : CustomYieldInstruction
{
public override bool keepWaiting
{
get
{
return !Input.GetMouseButtonDown(1);
}
}

public WaitForMouseDown()
{
Debug.Log("Waiting for Mouse right button down");
}
}

协程原理

如果代码中对gameObject.SetActive(false),协程就会失效,即使再次激活,也不能继续执行。原因是协程是在StartCoroutine时被注册到的GameObject上,他的生命期受限于GameObject的生命期,因此受GameObject是否active的影响。

不难得出,协程和Update一样是在每一帧被调用执行的。经过测试,一般是在LastUpdate之后执行的。

而具体怎么执行,是利用了迭代器:每一帧检测yield的返回情况(想想StartCoroutine估计就是while(MoveNext))。也就是每一帧都执行MoveNext,如果为true下一帧就继续执行MoveNext,如果为false就结束协程将其从协程队列中剔除。

自己实现携程

// 当然也可以考虑用async-await替代协程,ETTask就是这样的。具体以后再看,粗看应该是最后将回调放到同步上下文.Post里做了。

找到一个不错的实践,转载一下。可以手动控制携程顺序、执行片长。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
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;
}
}
}