Unity 協程(Coroutine)原理與用法詳解

心之凌兒發表於2021-04-29

前言:

協程在Unity中是一個很重要的概念,我們知道,在使用Unity進行遊戲開發時,一般(注意是一般)不考慮多執行緒,那麼如何處理一些在主任務之外的需求呢,Unity給我們提供了協程這種方式

為啥在Unity中一般不考慮多執行緒

  • 因為在Unity中,只能在主執行緒中獲取物體的元件、方法、物件,如果脫離這些,Unity的很多功能無法實現,那麼多執行緒的存在與否意義就不大了

既然這樣,執行緒與協程有什麼區別呢:

  • 對於協程而言,同一時間只能執行一個協程,而執行緒則是併發的,可以同時有多個執行緒在執行
  • 兩者在記憶體的使用上是相同的,共享堆,不共享棧

其實對於兩者最關鍵,最簡單的區別是微觀上執行緒是並行的,而協程是序列的,如果你不理解沒有關係,通過下面的解釋你就明白了

關於協程

1,什麼是協程

協程,從字面意義上理解就是協助程式的意思,我們在主任務進行的同時,需要一些分支任務配合工作來達到最終的效果

稍微形象的解釋一下,想象一下,在進行主任務的過程中我們需要一個對資源消耗極大的操作時候,如果在一幀中實現這樣的操作,遊戲就會變得十分卡頓,這個時候,我們就可以通過協程,在一定幀內完成該工作的處理,同時不影響主任務的進行

2,協程的原理

首先需要了解協程不是執行緒,協程依舊是在主執行緒中進行

然後要知道協程是通過迭代器來實現功能的,通過關鍵字IEnumerator來定義一個迭代方法,注意使用的是IEnumerator,而不是IEnumerable:

兩者之間的區別:

  • IEnumerator:是非泛型的,也是協程認可的引數
  • IEnumerable:通過泛型實現的迭代器,協程不使用該迭代器

在迭代器中呢,最關鍵的是yield 的使用,這是實現我們協程功能的主要途徑,通過該關鍵方法,可以使得協程的執行暫停、記錄下一次啟動的時間與位置等等:

關於迭代器的具體解釋:

由於yield 在協程中的特殊性,與關鍵性,我們到後面在單獨解釋,先介紹一下協程如何通過程式碼實現

3、協程的使用

首先通過一個迭代器定義一個返回值為IEnumerator的方法,然後再程式中通過StartCoroutine來開啟一個協程即可:

在正式開始程式碼之前,需要了解StartCoroutine的兩種過載方式:

  • StartCoroutine(string methodName:這種是沒有引數的情況,直接通過方法名(字串形式)來開啟協程
  • StartCoroutine(IEnumerator routine:通過方法形式呼叫
  • StartCoroutine(string methodName,object values):帶引數的通過方法名進行呼叫

協程開啟的方式主要是上面的三種形式,如果你還是不理解,可以檢視下面程式碼:

 	//通過迭代器定義一個方法
 	IEnumerator Demo(int i)
    {
        //程式碼塊

        yield return 0; 
		//程式碼塊
       
    }

    //在程式種呼叫協程
    public void Test()
    {
        //第一種與第二種呼叫方式,通過方法名與引數呼叫
        StartCoroutine("Demo", 1);

        //第三種呼叫方式, 通過呼叫方法直接呼叫
        StartCoroutine(Demo(1));
    }

在一個協程開始後,同樣會對應一個結束協程的方法StopCoroutineStopAllCoroutines兩種方式,但是需要注意的是,兩者的使用需要遵循一定的規則,在介紹規則之前,同樣介紹一下關於StopCoroutine過載:

  • StopCoroutine(string methodName:通過方法名(字串)來進行
  • StopCoroutine(IEnumerator routine:通過方法形式來呼叫
  • StopCoroutine(Coroutine routine):通過指定的協程來關閉

剛剛我們說到他們的使用是有一定的規則的,那麼規則是什麼呢,答案是前兩種結束協程方法的使用上,如果我們是使用StartCoroutine(string methodName)來開啟一個協程的,那麼結束協程就只能使用StopCoroutine(string methodName)StopCoroutine(Coroutine routine)來結束協程,可以在文件中找到這句話:

在這裡插入圖片描述

4、關於yield

在上面,我們已經知道yield 的關鍵性,要想理解協程,就要理解yield

如果你瞭解Unity的指令碼的生命週期,你一定對yield 這幾個關鍵詞很熟悉,沒錯,yield 也是指令碼生命週期的一些執行方法,不同的yield 的方法處於生命週期的不同位置,可以通過下圖檢視:

在這裡插入圖片描述
通過這張圖可以看出大部分yield位置UpdateLateUpdate之間,而一些特殊的則分佈在其他位置,這些yield 代表什麼意思呢,又為啥位於這個位置呢


首先解釋一下位於UpdateLateUpdate之間這些yield 的含義:

  • yield return null; 暫停協程等待下一幀繼續執行

  • yield return 0或其他數字; 暫停協程等待下一幀繼續執行

  • yield return new WairForSeconds(時間); 等待規定時間後繼續執行

  • yield return StartCoroutine("協程方法名");開啟一個協程(巢狀協程)

在瞭解這些yield的方法後,可以通過下面的程式碼來理解其執行順序:

 void Update()
    {
        Debug.Log("001");
        StartCoroutine("Demo");
        Debug.Log("003");

    }
    private void LateUpdate()
    {
        Debug.Log("005");
    }

    IEnumerator Demo()
    {
        Debug.Log("002");

        yield return 0;
        Debug.Log("004");
    }

將上面的指令碼掛載到物體上,執行遊戲場景,來檢視列印的日誌,可以看到下面的日誌記錄:

在這裡插入圖片描述
可以很清晰的看出,協程雖然是在Update中開啟,但是關於yield return null後面的程式碼會在下一幀執行,並且是在Update執行完之後才開始執行,但是會在LateUpdate之前執行


接下來看幾個特殊的yield,他們是用在一些特殊的區域,一般不會有機會去使用,但是對於某些特殊情況的應對會很方便

  • yield return GameObject; 當遊戲物件被獲取到之後執行
  • yield return new WaitForFixedUpdate():等到下一個固定幀數更新
  • yield return new WaitForEndOfFrame():等到所有相機畫面被渲染完畢後更新
  • yield break; 跳出協程對應方法,其後面的程式碼不會被執行

通過上面的一些yield一些用法以及其在指令碼生命週期中的位置,我們也可以看到關於協程不是執行緒的概念的具體的解釋,所有的這些方法都是在主執行緒中進行的,只是有別於我們正常使用的UpdateLateUpdate這些可視的方法

5、執行緒幾個小用法

5.1、將一個複雜程式分幀執行:

如果一個複雜的函式對於一幀的效能需求很大,我們就可以通過yield return null將步驟拆除,從而將效能壓力分攤開來,最終獲取一個流暢的過程,這就是一個簡單的應用

舉一個案例,如果某一時刻需要使用Update讀取一個列表,這樣一般需要一個迴圈去遍歷列表,這樣每幀的程式碼執行量就比較大,就可以將這樣的執行放置到協程中來處理:

public class Test : MonoBehaviour
{
    public List<int> nums = new List<int> { 1, 2, 3, 4, 5, 6 };


    private void Update()
    {
        if(Input.GetKeyDown(KeyCode.Space))
        {
            StartCoroutine(PrintNum(nums));
        }
    }
	//通過協程分幀處理
    IEnumerator PrintNum(List<int> nums)
    {
        foreach(int i in nums)
        {
            Debug.Log(i);
            yield return null;
                 
        }

    }
}

上面只是列舉了一個小小的案例,在實際工作中會有一些很消耗效能的操作的時候,就可以通過這樣的方式來進行效能消耗的分消

5.2、進行計時器工作

當然這種應用場景很少,如果我們需要計時器有很多其他更好用的方式,但是你可以瞭解是存在這樣的操作的,要實現這樣的效果,需要通過yield return new WaitForSeconds()的延時執行的功能:

	IEnumerator Test()
    {
        Debug.Log("開始");
        yield return new WaitForSeconds(3);
        Debug.Log("輸出開始後三秒後執行我");
    }

5.3、非同步載入等功能

只要一說到非同步,就必定離不開協程,因為在非同步載入過程中可能會影響到其他任務的程式,這個時候就需要通過協程將這些可能被影響的任務剝離出來

常見的非同步操作有:

  • AB包資源的非同步載入
  • Reaources資源的非同步載入
  • 場景的非同步載入
  • WWW模組的非同步請求

這些非同步操作的實現都需要協程的支援,可以通過我之前的一篇場景載入介面實現的文章來理解該內容:

關於非同步的文章:

總結

通過上面的一些操作,相信你應該理解協程的基本原理與用法,以及一些相關的小知識

因為協程本身也是一個比較複雜的概念,所以我的理解也可能有錯誤的地方,如果你發現文章中有哪些不正確的地方,歡迎留言指出< ^ _ ^ >

相關文章